From cde90ccbca13dbc996d4e956457f49e2d5be1e42 Mon Sep 17 00:00:00 2001 From: Donzasto Date: Mon, 19 Feb 2024 09:00:24 +0300 Subject: [PATCH 001/265] Fix TextBlock wrong caret position (#14627) --- src/Avalonia.Controls/TextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 14e2b3e001..50abfd23c8 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -923,6 +923,8 @@ namespace Avalonia.Controls } else if (change.Property == SelectionEndProperty) { + _presenter?.MoveCaretToTextPosition(CaretIndex); + OnSelectionEndChanged(change); } else if (change.Property == MaxLinesProperty) From 1e078c44eaf0e7a0dacb1eb8620b8ae14f1f4d0e Mon Sep 17 00:00:00 2001 From: Bas Date: Mon, 19 Feb 2024 22:17:39 +0100 Subject: [PATCH 002/265] added _windowProperties override (#14649) --- src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index e08f1b28be..509be65784 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -1,13 +1,21 @@ using System; -using System.ComponentModel; -using System.Runtime.InteropServices; -using Avalonia.Platform; +using Avalonia.Controls; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { class EmbeddedWindowImpl : WindowImpl { + public EmbeddedWindowImpl() + { + _windowProperties = new WindowProperties + { + ShowInTaskbar = false, + IsResizable = false, + Decorations = SystemDecorations.None + }; + } + protected override IntPtr CreateWindowOverride(ushort atom) { var hWnd = UnmanagedMethods.CreateWindowEx( From f3f26eb113ceec63e310115d34a8f02fe8e86b51 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 20 Feb 2024 11:47:47 +0600 Subject: [PATCH 003/265] Introduced a way to lease the underlying platform graphics context from Skia context (#14652) * Introduced a way to lease the underlying platform graphics context from Skia context * API suppression --- api/Avalonia.Skia.nupkg.xml | 10 ++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 54 +++++++++++++++++-- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 8 +++ .../Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs | 4 +- .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 4 +- .../ISkiaSharpApiLeaseFeature.cs | 8 +++ 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 api/Avalonia.Skia.nupkg.xml diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml new file mode 100644 index 0000000000..6348b8f033 --- /dev/null +++ b/api/Avalonia.Skia.nupkg.xml @@ -0,0 +1,10 @@ + + + + + CP0006 + M:Avalonia.Skia.ISkiaSharpApiLease.TryLeasePlatformGraphicsApi + baseline/netstandard2.0/Avalonia.Skia.dll + target/netstandard2.0/Avalonia.Skia.dll + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 32a2edeebb..df82e7635a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -99,6 +99,7 @@ namespace Avalonia.Skia private readonly DrawingContextImpl _context; private readonly SKMatrix _revertTransform; private bool _isDisposed; + private bool _leased; public ApiLease(DrawingContextImpl context) { @@ -107,11 +108,26 @@ namespace Avalonia.Skia _context._leased = true; } - public SKCanvas SkCanvas => _context.Canvas; + void CheckLease() + { + if (_leased) + throw new InvalidOperationException("The underlying graphics API is currently leased"); + } + + T CheckLease(T rv) + { + CheckLease(); + return rv; + } + + public SKCanvas SkCanvas => CheckLease(_context.Canvas); + // GrContext is accessible during the lease since one might want to wrap native resources + // Into Skia ones public GRContext? GrContext => _context.GrContext; - public SKSurface? SkSurface => _context.Surface; - public double CurrentOpacity => _context._currentOpacity; - + public SKSurface? SkSurface => CheckLease(_context.Surface); + public double CurrentOpacity => CheckLease(_context._currentOpacity); + + public void Dispose() { if (!_isDisposed) @@ -121,6 +137,36 @@ namespace Avalonia.Skia _isDisposed = true; } } + + class PlatformApiLease : ISkiaSharpPlatformGraphicsApiLease + { + private readonly ApiLease _parent; + + public PlatformApiLease(ApiLease parent, IPlatformGraphicsContext context) + { + _parent = parent; + _parent.GrContext?.Flush(); + Context = context; + _parent._leased = true; + } + + public void Dispose() + { + _parent._leased = false; + _parent.GrContext?.ResetContext(); + } + + public IPlatformGraphicsContext Context { get; } + } + + public ISkiaSharpPlatformGraphicsApiLease? TryLeasePlatformGraphicsApi() + { + CheckLease(); + if (_context._gpu is ISkiaGpuWithPlatformGraphicsContext gpu && + gpu.PlatformGraphicsContext is { } context) + return new PlatformApiLease(this, context); + return null; + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index e6e30a1203..20e7adb334 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Metadata; using Avalonia.Platform; using SkiaSharp; @@ -24,6 +25,13 @@ namespace Avalonia.Skia /// An optional custom render session. ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session); } + + //TODO12: Merge into ISkiaGpu + [Unstable] + public interface ISkiaGpuWithPlatformGraphicsContext : ISkiaGpu + { + IPlatformGraphicsContext? PlatformGraphicsContext { get; } + } public interface ISkiaSurface : IDisposable { diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index 8965f4fc7a..8e5573a465 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Metal; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Metal; -internal class SkiaMetalGpu : ISkiaGpu +internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext { private SkiaMetalApi _api = new(); private GRContext? _context; @@ -31,6 +32,7 @@ internal class SkiaMetalGpu : ISkiaGpu public bool IsLost => false; public IDisposable EnsureCurrent() => _device.EnsureCurrent(); + public IPlatformGraphicsContext? PlatformGraphicsContext => _device; public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index d403855094..00752f8b2b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -10,7 +10,8 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - internal class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + internal class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature, + ISkiaGpuWithPlatformGraphicsContext { private readonly GRContext _grContext; private readonly IGlContext _glContext; @@ -152,6 +153,7 @@ namespace Avalonia.Skia public bool IsLost => _glContext.IsLost; public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); + public IPlatformGraphicsContext? PlatformGraphicsContext => _glContext; public object? TryGetFeature(Type featureType) { diff --git a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs index 66abd818e6..1e56ee520b 100644 --- a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs +++ b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Metadata; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia; @@ -17,4 +18,11 @@ public interface ISkiaSharpApiLease : IDisposable GRContext? GrContext { get; } SKSurface? SkSurface { get; } double CurrentOpacity { get; } + ISkiaSharpPlatformGraphicsApiLease? TryLeasePlatformGraphicsApi(); +} + +[Unstable] +public interface ISkiaSharpPlatformGraphicsApiLease : IDisposable +{ + IPlatformGraphicsContext Context { get; } } From 7fb26639f5d7bcc5369651675d4c401e6406273d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 20 Feb 2024 11:48:55 +0600 Subject: [PATCH 004/265] GPU interop features now don't require to skip the first frame and available earlier in general (#14651) --- .../Avalonia.Android/AndroidPlatform.cs | 1 + .../Platform/IPlatformRenderInterface.cs | 5 ++ .../Rendering/Composition/Compositor.cs | 64 +++++++++++++------ .../Server/ServerCompositor.UserApis.cs | 50 +++++++++++++++ .../Composition/Server/ServerCompositor.cs | 6 ++ .../PlatformRenderInterfaceContextManager.cs | 11 +++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 + src/Avalonia.X11/X11Platform.cs | 1 + .../HeadlessPlatformRenderInterface.cs | 1 + src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 18 ++++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 1 + src/Windows/Avalonia.Win32/Win32Platform.cs | 1 + src/iOS/Avalonia.iOS/Platform.cs | 1 + 13 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index b991d8067f..f3dcb3560c 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -90,6 +90,7 @@ namespace Avalonia.Android } Compositor = new Compositor(graphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } private static IPlatformGraphics InitializeGraphics(AndroidPlatformOptions opts) diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 57fedb3d69..c783a5ea65 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -217,5 +217,10 @@ namespace Avalonia.Platform /// Indicates that the context is no longer usable. This method should be thread-safe /// bool IsLost { get; } + + /// + /// Exposes features that should be available for consumption while context isn't active (e. g. from the UI thread) + /// + IReadOnlyDictionary PublicFeatures { get; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 24817d7865..b8b0d73679 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Animation.Easings; +using Avalonia.Controls; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Platform; @@ -256,33 +257,56 @@ namespace Avalonia.Rendering.Composition return tcs.Task; } + internal ValueTask> GetRenderInterfacePublicFeatures() + { + if (Server.AT_TryGetCachedRenderInterfaceFeatures() is { } rv) + return new(rv); + if (!Loop.RunsInBackground) + return new(Server.RT_GetRenderInterfaceFeatures()); + return new(InvokeServerJobAsync(Server.RT_GetRenderInterfaceFeatures)); + } + /// /// Attempts to query for a feature from the platform render interface /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => - new(InvokeServerJobAsync(() => - { - using (Server.RenderInterface.EnsureCurrent()) - { - return Server.RenderInterface.Value.TryGetFeature(featureType); - } - })); + public async ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + (await GetRenderInterfacePublicFeatures().ConfigureAwait(false)).TryGetValue(featureType, out var rv); + return rv; + } + + /// + /// Attempts to query for GPU interop feature from the platform render interface + /// + /// + public async ValueTask TryGetCompositionGpuInterop() + { + var externalObjects = + (IExternalObjectsRenderInterfaceContextFeature?)await TryGetRenderInterfaceFeature( + typeof(IExternalObjectsRenderInterfaceContextFeature)).ConfigureAwait(false); - public ValueTask TryGetCompositionGpuInterop() => - new(InvokeServerJobAsync(() => - { - using (Server.RenderInterface.EnsureCurrent()) - { - var feature = Server.RenderInterface.Value - .TryGetFeature(); - if (feature == null) - return null; - return new CompositionInterop(this, feature); - } - })); + if (externalObjects == null) + return null; + return new CompositionInterop(this, externalObjects); + } internal bool UnitTestIsRegisteredForSerialization(ICompositorSerializable serializable) => _objectSerializationHashSet.Contains(serializable); + + /// + /// Attempts to get the Compositor instance that will be used by default for new s + /// created by the current platform backend. + /// + /// This won't work for every single platform backend and backend settings, e. g. with web we'll need to have + /// separate Compositor instances per output HTML canvas since they don't share OpenGL state. + /// Another case where default compositor won't be available is our planned multithreaded rendering mode + /// where each window would get its own Compositor instance + /// + /// This method is still useful for obtaining GPU device LUID to speed up initialization, but you should + /// always check if default Compositor matches one used by our control once it gets attached to a TopLevel + /// + /// + public static Compositor? TryGetDefaultCompositor() => AvaloniaLocator.Current.GetService(); } internal interface ICompositorScheduler diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs new file mode 100644 index 0000000000..bbdea64004 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositor +{ + private IReadOnlyDictionary? _renderInterfaceFeatureCache; + private readonly object _renderInterfaceFeaturesUserApiLock = new(); + + void RT_OnContextCreated(IPlatformRenderInterfaceContext context) + { + lock (_renderInterfaceFeaturesUserApiLock) + { + _renderInterfaceFeatureCache = null; + _renderInterfaceFeatureCache = context.PublicFeatures.ToDictionary(x => x.Key, x => x.Value); + } + } + + bool RT_OnContextLostExceptionFilterObserver(Exception e) + { + if (e is PlatformGraphicsContextLostException) + { + lock (_renderInterfaceFeaturesUserApiLock) + _renderInterfaceFeatureCache = null; + } + return false; + } + + void RT_OnContextDisposed() + { + lock (_renderInterfaceFeaturesUserApiLock) + _renderInterfaceFeatureCache = null; + } + + public IReadOnlyDictionary? AT_TryGetCachedRenderInterfaceFeatures() + { + lock (_renderInterfaceFeaturesUserApiLock) + return _renderInterfaceFeatureCache; + } + + public IReadOnlyDictionary RT_GetRenderInterfaceFeatures() + { + lock (_renderInterfaceFeaturesUserApiLock) + return _renderInterfaceFeatureCache ??= RenderInterface.Value.PublicFeatures; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f1bc865475..c1f286e827 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -44,6 +44,8 @@ namespace Avalonia.Rendering.Composition.Server { _renderLoop = renderLoop; RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); + RenderInterface.ContextDisposed += RT_OnContextDisposed; + RenderInterface.ContextCreated += RT_OnContextCreated; BatchObjectPool = batchObjectPool; BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); @@ -187,6 +189,10 @@ namespace Avalonia.Rendering.Composition.Server _safeThread = Thread.CurrentThread; RenderCore(); } + catch (Exception e) when (RT_OnContextLostExceptionFilterObserver(e) && false) + // Will never get here, only using exception filter side effect + { + } finally { NotifyBatchesRendered(); diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 82dcd8f184..d6576511b9 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -11,6 +11,8 @@ internal class PlatformRenderInterfaceContextManager private readonly IPlatformGraphics? _graphics; private IPlatformRenderInterfaceContext? _backend; private OwnedDisposable? _gpuContext; + public event Action? ContextDisposed; + public event Action? ContextCreated; public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) { @@ -23,8 +25,12 @@ internal class PlatformRenderInterfaceContextManager { _backend?.Dispose(); _backend = null; - _gpuContext?.Dispose(); - _gpuContext = null; + if (_gpuContext != null) + { + _gpuContext?.Dispose(); + _gpuContext = null; + ContextDisposed?.Invoke(); + } if (_graphics != null) { @@ -36,6 +42,7 @@ internal class PlatformRenderInterfaceContextManager _backend = AvaloniaLocator.Current.GetRequiredService() .CreateBackendContext(_gpuContext?.Value); + ContextCreated?.Invoke(_backend); } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 3f9f5ad0b3..85331bea6b 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -161,6 +161,7 @@ namespace Avalonia.Native Compositor = new Compositor(_platformGraphics, true); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index b7444c69c6..75e0776b5e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -98,6 +98,7 @@ namespace Avalonia.X11 } Compositor = new Compositor(graphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } public IntPtr DeferredDisplay { get; set; } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 1d4bae7e6a..da46b45998 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -60,6 +60,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 51e182f7e3..555c564f4a 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.OpenGL; using Avalonia.Platform; namespace Avalonia.Skia; @@ -14,6 +15,22 @@ internal class SkiaContext : IPlatformRenderInterfaceContext public SkiaContext(ISkiaGpu? gpu) { _gpu = gpu; + + var features = new Dictionary(); + + if (gpu != null) + { + void TryFeature() where T : class + { + if (gpu!.TryGetFeature() is { } feature) + features!.Add(typeof(T), feature); + } + // TODO12: extend ISkiaGpu with PublicFeatures instead + TryFeature(); + TryFeature(); + } + + PublicFeatures = features; } public void Dispose() @@ -44,6 +61,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext } public bool IsLost => _gpu?.IsLost ?? false; + public IReadOnlyDictionary PublicFeatures { get; } public object? TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 7750a8ed4e..1c2682607d 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -181,6 +181,7 @@ namespace Avalonia.Direct2D1 public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); public bool IsLost => false; + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 3580426dfd..56da4ccb88 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -127,6 +127,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); s_compositor = new Compositor( platformGraphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_compositor); } public event EventHandler? ShutdownRequested; diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 1192bb962c..07c212a01a 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -78,6 +78,7 @@ namespace Avalonia.iOS .Bind().ToConstant(keyboard); Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } private static IPlatformGraphics InitializeGraphics(iOSPlatformOptions opts) From d6f5e7ddb622b745e51c929da4a9b9300522c65b Mon Sep 17 00:00:00 2001 From: Rosentti Date: Tue, 20 Feb 2024 09:33:29 +0200 Subject: [PATCH 005/265] IsVisible for NativeMenuItem(s) (#14567) * IsVisible for nativemenuitems * implementation for windows * Update native/Avalonia.Native/src/OSX/menu.mm Co-authored-by: jp2masa * Update menu.h * address review changes * add hidden option to ControlCatalog --------- Co-authored-by: jp2masa Co-authored-by: Max Katz --- native/Avalonia.Native/src/OSX/menu.h | 4 +++- native/Avalonia.Native/src/OSX/menu.mm | 11 +++++++++++ samples/ControlCatalog/App.xaml | 1 + src/Avalonia.Controls/NativeMenuBarPresenter.cs | 1 + src/Avalonia.Controls/NativeMenuItem.cs | 15 +++++++++++++++ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 7 +++++++ src/Avalonia.Native/IAvnMenuItem.cs | 6 ++++++ src/Avalonia.Native/avn.idl | 1 + 8 files changed, 45 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 1bea2ccc8d..eb706d3e07 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -51,7 +51,9 @@ public: virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; virtual HRESULT SetIsChecked (bool isChecked) override; - + + virtual HRESULT SetIsVisible (bool isVisible) override; + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override; virtual HRESULT SetIcon (void* data, size_t length) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 3905987aab..7a7edcb1cb 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -205,6 +205,17 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) } } +HRESULT AvnAppMenuItem::SetIsVisible (bool isVisible) +{ + START_COM_CALL; + + @autoreleasepool + { + [_native setHidden:!isVisible]; + return S_OK; + } +} + HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) { START_COM_CALL; diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 519fb3ec7a..3999191696 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -75,6 +75,7 @@ + diff --git a/src/Avalonia.Controls/NativeMenuBarPresenter.cs b/src/Avalonia.Controls/NativeMenuBarPresenter.cs index be93f9dc85..c5637d3dd4 100644 --- a/src/Avalonia.Controls/NativeMenuBarPresenter.cs +++ b/src/Avalonia.Controls/NativeMenuBarPresenter.cs @@ -26,6 +26,7 @@ internal class NativeMenuBarPresenter : Menu [!MenuItem.IconProperty] = nativeItem.GetObservable(NativeMenuItem.IconProperty) .Select(i => i is { } bitmap ? new Image { Source = bitmap } : null).ToBinding(), [!MenuItem.IsEnabledProperty] = nativeItem.GetObservable(NativeMenuItem.IsEnabledProperty).ToBinding(), + [!MenuItem.IsVisibleProperty] = nativeItem.GetObservable(NativeMenuItem.IsVisibleProperty).ToBinding(), [!MenuItem.CommandProperty] = nativeItem.GetObservable(NativeMenuItem.CommandProperty).ToBinding(), [!MenuItem.CommandParameterProperty] = nativeItem.GetObservable(NativeMenuItem.CommandParameterProperty).ToBinding(), diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 72f7102768..d67e6a58fc 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -144,6 +144,21 @@ namespace Avalonia.Controls set => SetValue(IsEnabledProperty, value); } + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + Visual.IsVisibleProperty.AddOwner(); + + /// + /// Gets or sets a value indicating whether this menu item is visible. + /// + public bool IsVisible + { + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + void CanExecuteChanged() { SetCurrentValue(IsEnabledProperty, Command?.CanExecute(CommandParameter) ?? true); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index ef9b7c26c3..fcc118bd31 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -243,6 +243,13 @@ namespace Avalonia.FreeDesktop return new DBusVariantItem("b", new DBusBoolItem(false)); return null; } + + if (name == "visible") { + if (!item.IsVisible) + return new DBusVariantItem("b", new DBusBoolItem(false)); + return new DBusVariantItem("b", new DBusBoolItem(true)); + } + if (name == "shortcut") { if (item.Gesture is null) diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index 50f64decca..275a088821 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -40,6 +40,7 @@ namespace Avalonia.Native.Interop.Impl private void UpdateToolTip(string toolTip) => SetToolTip(toolTip ?? ""); + private void UpdateIsVisible(bool isVisible) => SetIsVisible(isVisible.AsComBool()); private void UpdateIsChecked(bool isChecked) => SetIsChecked(isChecked.AsComBool()); private void UpdateToggleType(NativeMenuItemToggleType toggleType) @@ -121,6 +122,8 @@ namespace Avalonia.Native.Interop.Impl UpdateIsChecked(item.IsChecked); + UpdateIsVisible(item.IsVisible); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) .Subscribe(x => UpdateTitle(x))); @@ -139,6 +142,9 @@ namespace Avalonia.Native.Interop.Impl _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) .Subscribe(x => UpdateIsChecked(x))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsVisibleProperty) + .Subscribe(x => UpdateIsVisible(x))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty) .Subscribe(x => UpdateIcon(x))); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 27da2fc894..3a715c33ee 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1026,6 +1026,7 @@ interface IAvnMenuItem : IUnknown HRESULT SetGesture(AvnKey key, AvnInputModifiers modifiers); HRESULT SetAction(IAvnPredicateCallback* predicate, IAvnActionCallback* callback); HRESULT SetIsChecked(bool isChecked); + HRESULT SetIsVisible(bool isVisible); HRESULT SetToggleType(AvnMenuItemToggleType toggleType); HRESULT SetIcon(void* data, size_t length); } From 628a62e250062204ca1954429a2af8858520075c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 20 Feb 2024 02:53:28 -0800 Subject: [PATCH 006/265] Implement MacOSProperties.IsTemplateIcon attached property on TrayIcon (#14348) * Implement MacOS.IsTemplateIcon attached property on TrayIcon * Use MacOS.IsTemplateIcon in the ControlCatalog * Rename MacOS to MacOSProperties * Extract IsTemplateIcon to ITrayIconWithIsTemplateImpl --- native/Avalonia.Native/src/OSX/trayicon.h | 7 ++-- native/Avalonia.Native/src/OSX/trayicon.mm | 22 ++++++++++++ samples/ControlCatalog/App.xaml | 2 +- .../Platform/ITrayIconImpl.cs | 9 +++++ .../Platform/MacOSProperties.cs | 35 +++++++++++++++++++ src/Avalonia.Controls/TrayIcon.cs | 2 ++ src/Avalonia.Native/TrayIconImpl.cs | 7 +++- src/Avalonia.Native/avn.idl | 1 + src/Windows/Avalonia.Win32/TrayIconImpl.cs | 2 +- 9 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/MacOSProperties.cs diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index da30477005..86b61608c4 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -15,7 +15,8 @@ class AvnTrayIcon : public ComSingleObject { private: NSStatusItem* _native; - + bool _isTemplateIcon; + public: FORWARD_IUNKNOWN() @@ -28,8 +29,10 @@ public: virtual HRESULT SetMenu (IAvnMenu* menu) override; virtual HRESULT SetIsVisible (bool isVisible) override; - + virtual HRESULT SetToolTipText (char* text) override; + + virtual HRESULT SetIsTemplateIcon (bool isTemplateIcon) override; }; #endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 5b75b9cc19..917ff87694 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -45,6 +45,7 @@ HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) size.width = originalSize.width * scaleFactor; [image setSize: size]; + [image setTemplate: _isTemplateIcon]; [_native setImage:image]; } else @@ -98,3 +99,24 @@ HRESULT AvnTrayIcon::SetToolTipText(char* text) return S_OK; } + +HRESULT AvnTrayIcon::SetIsTemplateIcon(bool isTemplateIcon) +{ + START_COM_CALL; + + @autoreleasepool + { + if (_isTemplateIcon != isTemplateIcon) + { + _isTemplateIcon = isTemplateIcon; + + NSImage *image = [_native image]; + if (image) + { + [image setTemplate: _isTemplateIcon]; + } + } + } + + return S_OK; +} \ No newline at end of file diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3999191696..157009a5e3 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -64,7 +64,7 @@ - + diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 4ef9397d04..e6a4c7210d 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -32,4 +32,13 @@ namespace Avalonia.Platform /// Action? OnClicked { get; set; } } + + [Unstable] + public interface ITrayIconWithIsTemplateImpl : ITrayIconImpl + { + /// + /// Sets if the tray icon has a template/monochrome icon or not. + /// + void SetIsTemplateIcon(bool isTemplateIcon); + } } diff --git a/src/Avalonia.Controls/Platform/MacOSProperties.cs b/src/Avalonia.Controls/Platform/MacOSProperties.cs new file mode 100644 index 0000000000..00ecb374e2 --- /dev/null +++ b/src/Avalonia.Controls/Platform/MacOSProperties.cs @@ -0,0 +1,35 @@ +using Avalonia.Platform; + +namespace Avalonia.Controls; + +/// +/// Set of MacOS specific attached properties that allow deeper customization of the application per platform. +/// +public class MacOSProperties +{ + static MacOSProperties() + { + IsTemplateIconProperty.Changed.AddClassHandler(TrayIconIsTemplateIconChanged); + } + + /// + /// Defines the IsTemplateIcon attached property. + /// + public static readonly AttachedProperty IsTemplateIconProperty = + AvaloniaProperty.RegisterAttached("IsTemplateIcon"); + + /// + /// A Boolean value that determines whether the TrayIcon image represents a template image. + /// + public static void SetIsTemplateIcon(TrayIcon obj, bool value) => obj.SetValue(IsTemplateIconProperty, value); + + /// + /// Returns a Boolean value that indicates whether the TrayIcon image is a template image. + /// + public static bool GetIsTemplateIcon(TrayIcon obj) => obj.GetValue(IsTemplateIconProperty); + + private static void TrayIconIsTemplateIconChanged(TrayIcon trayIcon, AvaloniaPropertyChangedEventArgs args) + { + (trayIcon.Impl as ITrayIconWithIsTemplateImpl)?.SetIsTemplateIcon(args.GetNewValue()); + } +} diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index a675a549bc..1b91a08e34 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -182,6 +182,8 @@ namespace Avalonia.Controls public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter; + internal ITrayIconImpl? Impl => _impl; + private static void Lifetime_Exit(object? sender, ControlledApplicationLifetimeExitEventArgs e) { var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized."); diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index bac21d5811..4c9bed0e7c 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -8,7 +8,7 @@ using Avalonia.Platform; namespace Avalonia.Native { - internal class TrayIconImpl : ITrayIconImpl + internal class TrayIconImpl : ITrayIconWithIsTemplateImpl { private readonly IAvnTrayIcon _native; @@ -58,6 +58,11 @@ namespace Avalonia.Native _native.SetIsVisible(visible.AsComBool()); } + public void SetIsTemplateIcon(bool isTemplateIcon) + { + _native.SetIsTemplateIcon(isTemplateIcon.AsComBool()); + } + public INativeMenuExporter? MenuExporter { get; } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 3a715c33ee..87b95953bf 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1000,6 +1000,7 @@ interface IAvnTrayIcon : IUnknown HRESULT SetMenu(IAvnMenu* menu); HRESULT SetIsVisible(bool isVisible); HRESULT SetToolTipText(char* text); + HRESULT SetIsTemplateIcon(bool text); } [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index c7548ec336..df70515f99 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -104,7 +104,7 @@ namespace Avalonia.Win32 { UpdateIcon(!visible); } - + /// public void SetToolTipText(string? text) { From 72aa43dbf89cba4d5ea2cbe082d1ea851ed9c95c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 20 Feb 2024 08:45:36 -0800 Subject: [PATCH 007/265] Rename Win32SpecificOptions to Win32Properties (#14662) * Rename Win32SpecificOptions to Win32Properties * Update win32 project as well * Add missing doc --- .../Platform/IWin32OptionsTopLevelImpl.cs | 2 +- .../{Win32SpecificOptions.cs => Win32Properties.cs} | 8 ++++++-- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) rename src/Avalonia.Controls/Platform/{Win32SpecificOptions.cs => Win32Properties.cs} (91%) diff --git a/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs b/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs index 04ac0383e2..2e363e647d 100644 --- a/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Metadata; using Avalonia.Platform; -using static Avalonia.Controls.Platform.Win32SpecificOptions; +using static Avalonia.Controls.Win32Properties; namespace Avalonia.Controls.Platform { diff --git a/src/Avalonia.Controls/Platform/Win32SpecificOptions.cs b/src/Avalonia.Controls/Platform/Win32Properties.cs similarity index 91% rename from src/Avalonia.Controls/Platform/Win32SpecificOptions.cs rename to src/Avalonia.Controls/Platform/Win32Properties.cs index 53eb911e2e..13b1333ee6 100644 --- a/src/Avalonia.Controls/Platform/Win32SpecificOptions.cs +++ b/src/Avalonia.Controls/Platform/Win32Properties.cs @@ -4,13 +4,17 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Metadata; using Avalonia.Platform; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; -namespace Avalonia.Controls.Platform +namespace Avalonia.Controls { - public static class Win32SpecificOptions + /// + /// Set of Win32 specific properties and events that allow deeper customization of the application per platform. + /// + public static class Win32Properties { public delegate (uint style, uint exStyle) CustomWindowStylesCallback(uint style, uint exStyle); public delegate IntPtr CustomWndProcHookCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f2b536dc71..84e380a663 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -27,7 +27,7 @@ using System.Diagnostics; using Avalonia.Platform.Storage.FileIO; using Avalonia.Threading; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; -using static Avalonia.Controls.Platform.Win32SpecificOptions; +using static Avalonia.Controls.Win32Properties; namespace Avalonia.Win32 { From 8d446239cc1f27b18f37a07fb8312c25d3b14a34 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 20 Feb 2024 21:49:54 +0100 Subject: [PATCH 008/265] If the enforced lineheight is less than the regular line height adjust the ascent so the text is properly centered (#14669) --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index d9e6d6486d..6eff737444 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1336,6 +1336,13 @@ namespace Avalonia.Media.TextFormatting if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { + if (lineHeight < height) + { + var offset = Math.Max(0, height - lineHeight) / 2; + + ascent += offset; + } + height = lineHeight; } From 9cb21ee6908e2478db4db48255a165be33b49752 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 21 Feb 2024 00:38:56 +0100 Subject: [PATCH 009/265] Fix initial selection for AlwaysSelected (#14668) --- .../Primitives/SelectingItemsControl.cs | 13 +++++++- .../TabControlTests.cs | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 185af1a715..41960ac670 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -447,6 +447,12 @@ namespace Avalonia.Controls.Primitives { base.OnItemsViewCollectionChanged(sender!, e); + //Do not change SelectedIndex during initialization + if (_updateState is not null) + { + return; + } + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { SelectedIndex = 0; @@ -1217,7 +1223,7 @@ namespace Avalonia.Controls.Primitives _oldSelectedIndex = model.SelectedIndex; _oldSelectedItem = model.SelectedItem; - if (AlwaysSelected && model.Count == 0) + if (_updateState is null && AlwaysSelected && model.Count == 0) { model.SelectedIndex = 0; } @@ -1297,6 +1303,11 @@ namespace Avalonia.Controls.Primitives { SelectedItem = state.SelectedItem.Value; } + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 1ec612a85d..89bb79a276 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; @@ -18,6 +20,11 @@ namespace Avalonia.Controls.UnitTests { public class TabControlTests { + static TabControlTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + } + [Fact] public void First_Tab_Should_Be_Selected_By_Default() { @@ -435,6 +442,29 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Have_Initial_SelectedValue() + { + var xaml = @" + + + + "; + + var tabControl = (TabControl)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.Equal("World", tabControl.SelectedValue); + Assert.Equal(1, tabControl.SelectedIndex); + } + [Fact] public void Tab_Navigation_Should_Move_To_First_TabItem_When_No_Anchor_Element_Selected() { From 175673fa1dcd13b9c07060319e57231601580202 Mon Sep 17 00:00:00 2001 From: Ge Date: Wed, 21 Feb 2024 07:39:20 +0800 Subject: [PATCH 010/265] Fixes improper IME composition ignorance (#14664) --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index dc3b30adb4..b1f2a77ca0 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -127,7 +127,10 @@ namespace Avalonia.Win32.Input if (himc != IntPtr.Zero) { - _ignoreComposition = true; + if (IsComposing) + { + _ignoreComposition = true; + } if (_parent != null) { From 599cf1f2971fbbd998b412599e4d2081697da96c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 21 Feb 2024 12:09:18 +0100 Subject: [PATCH 011/265] Fix InlinesCollection Logical/VisualParent update (#14679) * Add failing test * Update Visual/LogicalTree when parents change --- .../Documents/InlineCollection.cs | 90 +++++++++++-------- .../TextBlockTests.cs | 19 ++++ 2 files changed, 74 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index dc6f828234..f3d32d92d3 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Collections; using Avalonia.LogicalTree; using Avalonia.Metadata; @@ -23,32 +24,8 @@ namespace Avalonia.Controls.Documents ResetBehavior = ResetBehavior.Remove; this.ForEachItem( - x => - { - x.InlineHost = InlineHost; - - LogicalChildren?.Add(x); - - if (x is InlineUIContainer container) - { - InlineHost?.VisualChildren.Add(container.Child); - } - - Invalidate(); - }, - x => - { - LogicalChildren?.Remove(x); - - if(x is InlineUIContainer container) - { - InlineHost?.VisualChildren.Remove(container.Child); - } - - x.InlineHost = null; - - Invalidate(); - }, + OnAdd, + OnRemove, () => throw new NotSupportedException()); } @@ -70,9 +47,11 @@ namespace Avalonia.Controls.Documents get => _inlineHost; set { + var oldValue = _inlineHost; + _inlineHost = value; - OnInlineHostChanged(value); + OnInlineHostChanged(oldValue, value); } } @@ -118,7 +97,7 @@ namespace Avalonia.Controls.Documents /// /// Adds a text segment to the collection. /// - /// For non complex content this appends the text to the end of currently held text. + /// For non-complex content this appends the text to the end of currently held text. /// For complex content this adds a to the collection. /// /// @@ -159,25 +138,66 @@ namespace Avalonia.Controls.Documents Invalidated?.Invoke(this, EventArgs.Empty); } - private void OnParentChanged(IAvaloniaList? oldParent, IAvaloniaList? newParent) + private void OnParentChanged(ICollection? oldValue, ICollection? newValue) { foreach (var child in this) { - if (oldParent != newParent) + if (Equals(oldValue, newValue)) { - oldParent?.Remove(child); - - newParent?.Add(child); + continue; } + + oldValue?.Remove(child); + + newValue?.Add(child); } + + Invalidate(); } - private void OnInlineHostChanged(IInlineHost? inlineHost) + private void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue) { foreach (var child in this) { - child.InlineHost = inlineHost; + if (child is not InlineUIContainer container) + { + continue; + } + + oldValue?.VisualChildren.Remove(container.Child); + + newValue?.VisualChildren.Add(container.Child); + } + + Invalidate(); + } + + private void OnAdd(TextElement inline) + { + inline.InlineHost = InlineHost; + + LogicalChildren?.Add(inline); + + if (inline is InlineUIContainer container) + { + InlineHost?.VisualChildren.Add(container.Child); } + + Invalidate(); + } + + private void OnRemove(TextElement inline) + { + LogicalChildren?.Remove(inline); + + if (inline is InlineUIContainer container) + { + InlineHost?.VisualChildren.Remove(container.Child); + } + + inline.InlineHost = null; + + Invalidate(); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 79a6706983..4e1379c5c2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -51,6 +51,25 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Changing_Inlines_Should_Attach_Embedded_Controls_To_Parents() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + var control = new Border(); + + var inlineUIContainer = new InlineUIContainer { Child = control }; + + target.Inlines = new InlineCollection { inlineUIContainer }; + + Assert.Equal(inlineUIContainer, control.Parent); + + Assert.Equal(target, control.VisualParent); + } + } + [Fact] public void Can_Call_Measure_Without_InvalidateTextLayout() { From 8730b66caea17e2cff55f9095079c5d2a05e3a8b Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Thu, 22 Feb 2024 02:52:57 +0100 Subject: [PATCH 012/265] Rewrite WindowImpl.Resize to use SetWindowPlacement (#14470) --- .../Interop/UnmanagedMethods.cs | 59 +++++++++++- .../Avalonia.Win32/PlatformConstants.cs | 4 + .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 24 ++--- src/Windows/Avalonia.Win32/WindowImpl.cs | 90 +++++++++++++++---- 4 files changed, 144 insertions(+), 33 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ae6b88f565..7b132f3857 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -105,18 +105,58 @@ namespace Avalonia.Win32.Interop public enum ShowWindowCommand { + /// + /// Hides the window and activates another window. + /// Hide = 0, + /// + /// Activates and displays a window. If the window is minimized, maximized, or arranged, the system restores it to its original + /// size and position. An application should specify this flag when displaying the window for the first time. + /// Normal = 1, + /// + /// Activates the window and displays it as a minimized window. + /// ShowMinimized = 2, + /// + /// Activates the window and displays it as a maximized window. + /// Maximize = 3, - ShowMaximized = 3, + /// + ShowMaximized = Maximize, + /// + /// Displays a window in its most recent size and position. This value is similar to , except that the window is not activated. + /// ShowNoActivate = 4, + /// + /// Activates the window and displays it in its current size and position. + /// Show = 5, + /// + /// Minimizes the specified window and activates the next top-level window in the Z order. + /// Minimize = 6, + /// + /// Displays the window as a minimized window. This value is similar to , except the window is not activated. + /// ShowMinNoActive = 7, + /// + /// Displays the window in its current size and position. This value is similar to , except that the window is not activated. + /// ShowNA = 8, + /// + /// Activates and displays the window. If the window is minimized, maximized, or arranged, the system restores it to its original size and position. + /// An application should specify this flag when restoring a minimized window. + /// Restore = 9, + /// + /// Sets the show state based on the value specified in the STARTUPINFO structure passed to the CreateProcess function + /// by the program that started the application. + /// ShowDefault = 10, + /// + /// Minimizes a window, even if the thread that owns the window is not responding. This flag should only be used when minimizing windows from a different thread. + /// ForceMinimize = 11 } @@ -1160,6 +1200,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", SetLastError = true)] public static extern bool AdjustWindowRectEx(ref RECT lpRect, uint dwStyle, bool bMenu, uint dwExStyle); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, WindowStyles dwStyle, bool bMenu, WindowStyles dwExStyle, uint dpi); + [DllImport("user32.dll")] public static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); @@ -1287,7 +1330,7 @@ namespace Avalonia.Win32.Interop public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + public static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); @@ -1374,6 +1417,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SetWindowPosFlags uFlags); [DllImport("user32.dll")] + public static extern bool SetWindowPlacement(IntPtr hWnd, in WINDOWPLACEMENT windowPlacement); + [DllImport("user32.dll")] public static extern bool SetFocus(IntPtr hWnd); [DllImport("user32.dll")] public static extern IntPtr GetFocus(); @@ -2240,6 +2285,14 @@ namespace Avalonia.Win32.Interop public int dwHoverTime; } + [Flags] + public enum WindowPlacementFlags : uint + { + SetMinPosition = 0x0001, + RestoreToMaximized = 0x0002, + AsyncWindowPlacement = 0x0004, + } + [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { @@ -2254,7 +2307,7 @@ namespace Avalonia.Win32.Interop /// /// Specifies flags that control the position of the minimized window and the method by which the window is restored. /// - public int Flags; + public WindowPlacementFlags Flags; /// /// The current show state of the window. diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index c638314c4d..9f5f152199 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -8,6 +8,10 @@ namespace Avalonia.Win32 public const string CursorHandleType = "HCURSOR"; public static readonly Version Windows10 = new Version(10, 0); + /// + /// Windows 10 Anniversary Update + /// + public static readonly Version Windows10_1607 = new Version(10, 0, 1607); public static readonly Version Windows8 = new Version(6, 2); public static readonly Version Windows8_1 = new Version(6, 3); public static readonly Version Windows7 = new Version(6, 1); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d164b7ca73..0070581e1d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -131,7 +131,7 @@ namespace Avalonia.Win32 { _dpi = (uint)wParam >> 16; var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = _dpi / 96.0; + _scaling = _dpi / StandardDpi; RefreshIcon(); ScalingChanged?.Invoke(_scaling); @@ -613,14 +613,6 @@ namespace Avalonia.Win32 { var size = (SizeCommand)wParam; - if (Resized != null && - (size == SizeCommand.Restored || - size == SizeCommand.Maximized)) - { - var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / RenderScaling, _resizeReason); - } - var windowState = size switch { SizeCommand.Maximized => WindowState.Maximized, @@ -629,10 +621,20 @@ namespace Avalonia.Win32 _ => WindowState.Normal, }; - if (windowState != _lastWindowState) + var stateChanged = windowState != _lastWindowState; + _lastWindowState = windowState; + + if (Resized != null && + (size == SizeCommand.Restored || + size == SizeCommand.Maximized)) { - _lastWindowState = windowState; + var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); + Resized(clientSize / RenderScaling, _resizeReason); + } + + if (stateChanged) + { var newWindowProperties = _windowProperties; newWindowProperties.WindowState = windowState; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 84e380a663..f2b1401ba6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -28,6 +28,7 @@ using Avalonia.Platform.Storage.FileIO; using Avalonia.Threading; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; using static Avalonia.Controls.Win32Properties; +using Avalonia.Logging; namespace Avalonia.Win32 { @@ -54,6 +55,11 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + /// + /// The Windows DPI which equates to a of 1.0. + /// + public const double StandardDpi = 96; + private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; private bool _isClientAreaExtended; @@ -287,8 +293,7 @@ namespace Avalonia.Win32 return WindowState.FullScreen; } - var placement = default(WINDOWPLACEMENT); - GetWindowPlacement(_hwnd, ref placement); + GetWindowPlacement(_hwnd, out var placement); return placement.ShowCmd switch { @@ -559,29 +564,59 @@ namespace Avalonia.Win32 public void Resize(Size value, WindowResizeReason reason) { - if (WindowState != WindowState.Normal) - return; - int requestedClientWidth = (int)(value.Width * RenderScaling); int requestedClientHeight = (int)(value.Height * RenderScaling); - GetClientRect(_hwnd, out var clientRect); + GetClientRect(_hwnd, out var currentClientRect); + if (currentClientRect.Width == requestedClientWidth && currentClientRect.Height == requestedClientHeight) + { + // Don't update our window position if the client size is already correct. This leads to Windows updating our + // "normal position" (i.e. restored bounds) to match our maximised or areo snap size, which is incorrect behaviour. + // We only want to proceed with this method if the new size is coming from Avalonia. + return; + } - // do comparison after scaling to avoid rounding issues - if (requestedClientWidth != clientRect.Width || requestedClientHeight != clientRect.Height) + if (_lastWindowState == WindowState.FullScreen) { - GetWindowRect(_hwnd, out var windowRect); + // Fullscreen mode is really a restored window without a frame filling the whole monitor. + // It doesn't make sense to resize the window in this state, so ignore this request. + Logger.TryGet(LogEventLevel.Warning, LogArea.Win32Platform)?.Log(this, "Ignoring resize event on fullscreen window."); + return; + } - using var scope = SetResizeReason(reason); - SetWindowPos( - _hwnd, - IntPtr.Zero, - 0, - 0, - requestedClientWidth + (_isClientAreaExtended ? 0 : windowRect.Width - clientRect.Width), - requestedClientHeight + (_isClientAreaExtended ? 0 : windowRect.Height - clientRect.Height), - SetWindowPosFlags.SWP_RESIZE); + GetWindowPlacement(_hwnd, out var windowPlacement); + + var clientScreenOrigin = new POINT(); + ClientToScreen(_hwnd, ref clientScreenOrigin); + + var requestedClientRect = new RECT + { + left = clientScreenOrigin.X, + right = clientScreenOrigin.X + requestedClientWidth, + + top = clientScreenOrigin.Y, + bottom = clientScreenOrigin.Y + requestedClientHeight, + }; + + var requestedWindowRect = _isClientAreaExtended ? requestedClientRect : ClientRectToWindowRect(requestedClientRect); + + if (requestedWindowRect.Width == windowPlacement.NormalPosition.Width && requestedWindowRect.Height == windowPlacement.NormalPosition.Height) + { + return; } + + windowPlacement.NormalPosition = requestedWindowRect; + + windowPlacement.ShowCmd = _lastWindowState switch + { + WindowState.Minimized => ShowWindowCommand.ShowMinNoActive, + WindowState.Maximized => ShowWindowCommand.ShowMaximized, + WindowState.Normal => ShowWindowCommand.ShowNoActivate, + _ => throw new NotImplementedException(), + }; + + using var scope = SetResizeReason(reason); + SetWindowPlacement(_hwnd, in windowPlacement); } public void Activate() @@ -913,7 +948,7 @@ namespace Avalonia.Win32 out _dpi, out _) == 0) { - _scaling = _dpi / 96.0; + _scaling = _dpi / StandardDpi; } } } @@ -1473,6 +1508,23 @@ namespace Avalonia.Win32 MF_BYCOMMAND | MF_ENABLED); } + private RECT ClientRectToWindowRect(RECT clientRect, WindowStyles? styleOverride = null, WindowStyles? extendedStyleOverride = null) + { + var style = styleOverride ?? GetStyle(); + var extendedStyle = extendedStyleOverride ?? GetExtendedStyle(); + + var result = Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607 + ? AdjustWindowRectEx(ref clientRect, (uint)style, false, (uint)extendedStyle) + : AdjustWindowRectExForDpi(ref clientRect, style, false, extendedStyle, (uint)(RenderScaling * StandardDpi)); + + if (!result) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + return clientRect; + } + #if USE_MANAGED_DRAG private Point ScreenToClient(Point point) { From a74a2849cf2cfed6da8bbcae377dc5ea60408683 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 22 Feb 2024 07:34:14 +0100 Subject: [PATCH 013/265] Use dynamic resources in expander button theme. (#14542) I'm not sure why, but the fluent `Expander` theme uses a mix of static and dynamic resources with seemingly no reason to differentiate them? It's not so much a problem for the resources in the default values for `Expander` as these can be overridden with local values, but for the toggle button and trigger styles, it means there's no way to customize these values. Converted all of the `StaticResource`s to `DynamicResource`s. --- .../Controls/Expander.xaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index b725fd67f8..55ed424e09 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -69,10 +69,10 @@ - + - - + + - + - + @@ -309,16 +309,16 @@ - + - + - + - + - - + + + + + + + + + + + - - +