diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml new file mode 100644 index 0000000000..6a7d53544e --- /dev/null +++ b/api/Avalonia.nupkg.xml @@ -0,0 +1,10 @@ + + + + + CP0006 + P:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.ImportCompleted + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + \ No newline at end of file diff --git a/build/SourceLink.props b/build/SourceLink.props index dd7ecc8d2a..b0f1f2c2cc 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -1,9 +1,9 @@ true - false + true + snupkg true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -14,10 +14,6 @@ true - - embedded - - diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 9cc9fc9523..88cdf4d9de 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -2,6 +2,7 @@ #include "AvnString.h" @interface AvnAppDelegate : NSObject -(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events; +-(void) releaseEvents; @end NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @@ -15,6 +16,11 @@ ComPtr _events; return self; } +- (void)releaseEvents +{ + _events = nil; +} + - (void)applicationWillFinishLaunching:(NSNotification *)notification { if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) @@ -105,6 +111,18 @@ extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDeleg } } +extern void ReleaseAvnAppEvents() +{ + NSApplication* app = [AvnApplication sharedApplication]; + id delegate = [app delegate]; + if ([delegate isMemberOfClass:[AvnAppDelegate class]]) + { + AvnAppDelegate* avnDelegate = delegate; + [avnDelegate releaseEvents]; + [app setDelegate:nil]; + } +} + HRESULT AvnApplicationCommands::HideApp() { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 5cf1b94a2f..672525c64a 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -38,6 +38,7 @@ extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate); +extern void ReleaseAvnAppEvents(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern NSRect ToNSRect (AvnRect r); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 1c7e2cf25a..3fddb72529 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -197,6 +197,14 @@ class AvaloniaNative : public ComSingleObject(); var right = new List(); - var suppressionFile = Path.Combine(suppressionFilesFolder, GetPackageId(packagePath) + ".nupkg.xml"); + var packageId = GetPackageId(packagePath); + var suppressionFile = Path.Combine(suppressionFilesFolder, packageId + ".nupkg.xml"); // Don't use Path.Combine with these left and right tool parameters. // Microsoft.DotNet.ApiCompat.Tool is stupid and treats '/' and '\' as different assemblies in suppression files. @@ -57,7 +58,7 @@ public static class ApiDiffValidation e.target == baselineDll.target && e.entry.Name == baselineDll.entry.Name); if (targetDll.entry is null) { - throw new InvalidOperationException($"Some assemblies are missing in the new package: {baselineDll.entry.Name} for {baselineDll.target}"); + throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}"); } var targetDllPath = $"target/{targetDll.target}/{targetDll.entry.Name}"; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index b82c446249..bbfc28aa9f 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -288,7 +288,7 @@ partial class Build : NukeBuild .Executes(async () => { await Task.WhenAll( - Directory.GetFiles(Parameters.NugetRoot).Select(nugetPackage => ApiDiffValidation.ValidatePackage( + Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffValidation.ValidatePackage( ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline, Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression))); }); diff --git a/nukebuild/Numerge b/nukebuild/Numerge index aef10ae67d..9738c6121f 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 +Subproject commit 9738c6121fdd143c78d3e25686a7e4e13c00f586 diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 43453833d7..64eccbbb3f 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -8,8 +8,8 @@ 1 net7.0 - - + + @@ -32,9 +32,9 @@ - - - + + + @@ -43,7 +43,5 @@ dirs.proj - - diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 33f22f4d02..6af35ff0d2 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -144,4 +144,30 @@ + + + Build + + + + + http://127.0.0.1:6001 + $(OutputPath)/$(AssemblyName).dll + MainWindow.axaml + $(APreviewExecutable) + $([System.IO.Path]::ChangeExtension('$(APreviewExecutable)', '.deps.json')) + $([System.IO.Path]::ChangeExtension('$(APreviewExecutable)', '.runtimeconfig.json')) + $([System.IO.Path]::GetFullPath('$(APreviewFile)')) + + + + + + + + + + + + diff --git a/readme.md b/readme.md index aa99360681..fc5e218a92 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ Considered by many to be the spiritual successor to WPF, Avalonia UI provides a For those seeking a cross-platform WPF, we have created [Avalonia XPF](https://avaloniaui.net/xpf), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform. #### Roadmap -To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). +To see the status of some of our features, look at our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). #### Breaking Changes You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index ddc2b7effb..16eb09de65 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -43,21 +43,15 @@ - - - - - - diff --git a/src/Avalonia.Base/Interactivity/EventRoute.cs b/src/Avalonia.Base/Interactivity/EventRoute.cs index 37539a7153..d0d82b4884 100644 --- a/src/Avalonia.Base/Interactivity/EventRoute.cs +++ b/src/Avalonia.Base/Interactivity/EventRoute.cs @@ -120,11 +120,6 @@ namespace Avalonia.Interactivity return; } - if (e.Source is null) - { - throw new ArgumentException("Event source may not be null", nameof(e)); - } - Interactive? lastTarget = null; var start = 0; var end = _route.Count; diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0f70386424..fcb2cec733 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -424,13 +424,13 @@ namespace Avalonia.Media /// public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) { + var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + if (characterHit.TrailingLength != 0) { - return new CharacterHit(characterHit.FirstCharacterIndex); + return previousCharacterHit; } - var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); - return new CharacterHit(previousCharacterHit.FirstCharacterIndex); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ca31d9a6d0..44f53420de 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting internal static Comparer TextBoundsComparer { get; } = Comparer.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); - private IReadOnlyList? _indexedTextRuns; + internal IReadOnlyList? _indexedTextRuns; private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Length == 0) + if (_textRuns.Length == 0 || _indexedTextRuns is null) { return new CharacterHit(); } - if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) - { - return nextCharacterHit; - } - - var lastTextPosition = FirstTextSourceIndex + Length; + var currentCharacterrHit = characterHit; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - // Can't move, we're after the last character - var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition); + var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition); - var currentRun = _textRuns[runIndex]; + var nextCharacterHit = characterHit; switch (currentRun) { case ShapedTextRun shapedRun: { - nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); + var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength); + + if (offset > 0) + { + currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength); + } + + nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit); + + if (offset > 0) + { + nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength); + } break; } - default: + case TextRun: { nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); break; } } - if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) { return characterHit; } @@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) { - if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) + if (_textRuns.Length == 0 || _indexedTextRuns is null) + { + return new CharacterHit(); + } + + if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex) + { + return new CharacterHit(FirstTextSourceIndex); + } + + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (characterIndex <= FirstTextSourceIndex) { - return previousCharacterHit; + return new CharacterHit(FirstTextSourceIndex); + } + + var currentCharacterrHit = characterHit; + + var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition); + + if (currentPosition == characterHit.FirstCharacterIndex) + { + currentRun = GetRunAtCharacterIndex(characterHit.FirstCharacterIndex, LogicalDirection.Backward, out currentPosition); + } + + var previousCharacterHit = characterHit; + + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster); + + if (offset > 0) + { + currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength); + } + + previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterrHit); + + if (offset > 0) + { + previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength); + } + break; + } + case TextRun: + { + if (characterHit.TrailingLength > 0) + { + previousCharacterHit = new CharacterHit(currentPosition, currentRun.Length); + + } + else + { + previousCharacterHit = new CharacterHit(currentPosition + currentRun.Length); + } + + break; + } } - if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex) + if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength) { - characterHit = new CharacterHit(FirstTextSourceIndex); + return characterHit; } - return characterHit; // Can't move, we're before the first character + return previousCharacterHit; } /// @@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { _textLineBreak = new TextLineBreak(textEndOfLine); - } - } - - /// - /// Tries to find the next character hit. - /// - /// The current character hit. - /// The next character hit. - /// - private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit) - { - nextCharacterHit = characterHit; - - var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var lastCodepointIndex = FirstTextSourceIndex + Length; - - if (codepointIndex >= lastCodepointIndex) - { - return false; // Cannot go forward anymore - } - - if (codepointIndex < FirstTextSourceIndex) - { - codepointIndex = FirstTextSourceIndex; - } - - var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - - while (runIndex < _textRuns.Length) - { - var currentRun = _textRuns[runIndex]; - - switch (currentRun) - { - case ShapedTextRun shapedRun: - { - var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); - - var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length; - - if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight) - { - nextCharacterHit = foundCharacterHit; - - return true; - } - - nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? - foundCharacterHit : - new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); - - if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) - { - return true; - } - - break; - } - default: - { - var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - if (textPosition == currentPosition) - { - nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); - - return true; - } - - break; - } - } - - currentPosition += currentRun.Length; - runIndex++; - } - - return false; - } - - /// - /// Tries to find the previous character hit. - /// - /// The current character hit. - /// The previous character hit. - /// - private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) - { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - if (characterIndex == FirstTextSourceIndex) - { - previousCharacterHit = new CharacterHit(FirstTextSourceIndex); - - return true; - } - - previousCharacterHit = characterHit; - - if (characterIndex < FirstTextSourceIndex) - { - return false; // Cannot go backward anymore. - } - - var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition); - - while (runIndex >= 0) - { - var currentRun = _textRuns[runIndex]; - - switch (currentRun) - { - case ShapedTextRun shapedRun: - { - var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); - - if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex) - { - previousCharacterHit = foundCharacterHit; - - return true; - } - - var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength; - - if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex) - { - previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); - } - - if (previousCharacterHit != characterHit) - { - return true; - } - - break; - } - default: - { - if (characterIndex == currentPosition + currentRun.Length) - { - previousCharacterHit = new CharacterHit(currentPosition); - - return true; - } - - break; - } - } - - currentPosition -= currentRun.Length; - runIndex--; } - - return false; } /// @@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting /// The logical direction. /// The text position of the found run index. /// The text run index. - private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) + private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) { var runIndex = 0; textPosition = FirstTextSourceIndex; + + if (_indexedTextRuns is null) + { + return null; + } + + TextRun? currentRun = null; TextRun? previousRun = null; - while (runIndex < _textRuns.Length) + while (runIndex < _indexedTextRuns.Count) { - var currentRun = _textRuns[runIndex]; + var indexedRun = _indexedTextRuns[runIndex]; + currentRun = indexedRun.TextRun; switch (currentRun) { @@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting { var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; - if (firstCluster > codepointIndex) - { - break; - } - - if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) - { - if (shapedRun.ShapedBuffer.IsLeftToRight) - { - if (firstCluster >= codepointIndex) - { - return --runIndex; - } - } - else - { - if (codepointIndex > firstCluster + currentRun.Length) - { - return --runIndex; - } - } - } + firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster); if (direction == LogicalDirection.Forward) { - if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) + if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } else { - if (codepointIndex > firstCluster && - codepointIndex <= firstCluster + currentRun.Length) + if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster) + { + textPosition -= previousRun.Length; + + return previousRun; + } + + if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; break; } - default: + case TextRun: { if (codepointIndex == textPosition) { - return runIndex; + return currentRun; } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; @@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting } runIndex++; + previousRun = currentRun; } - return runIndex; + return currentRun; } private TextLineMetrics CreateLineMetrics() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs index ce728f86a2..31cbaf2a29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Rendering.Composition; + +[NotClientImplementable] public interface ICompositionGpuInterop { /// @@ -84,14 +87,22 @@ public enum CompositionGpuImportedImageSynchronizationCapabilities /// /// An imported GPU object that's usable by composition APIs /// +[NotClientImplementable] public interface ICompositionGpuImportedObject : IAsyncDisposable { /// /// Tracks the import status of the object. Once the task is completed, /// the user code is allowed to free the resource owner in case when a non-owning - /// sharing handle was used + /// sharing handle was used. /// + Task ImportCompleted { get; } + + /// + /// ImportCompleted (recommended replacement) + [Obsolete("Please use ICompositionGpuImportedObject.ImportCompleted instead")] + [EditorBrowsable(EditorBrowsableState.Never)] Task ImportCompeted { get; } + /// /// Indicates if the device context this instance is associated with is no longer available /// diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs index 1643ec6e8d..12a252ed96 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs @@ -67,18 +67,20 @@ abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject Context = context; Feature = feature; - ImportCompeted = Compositor.InvokeServerJobAsync(Import); + ImportCompleted = Compositor.InvokeServerJobAsync(Import); } protected abstract void Import(); public abstract void Dispose(); - public Task ImportCompeted { get; } + public Task ImportCompleted { get; } + + public Task ImportCompeted => ImportCompleted; public bool IsLost => Context.IsLost; public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() => { - if (ImportCompeted.Status == TaskStatus.RanToCompletion) + if (ImportCompleted.Status == TaskStatus.RanToCompletion) Dispose(); })); } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs index cb5e21be00..97083ccaf5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs @@ -42,22 +42,31 @@ class ServerCompositionRenderData : SimpleServerRenderResource _items.Add(reader.ReadObject()); var collector = s_resourceHashSetPool.Get(); - foreach(var item in _items) - if (item is IRenderDataItemWithServerResources resourceItem) - resourceItem.Collect(collector); - + CollectResources(_items, collector); + foreach (var r in collector.Resources) { _referencedResources.Add(r); r.AddObserver(this); } - + collector.Resources.Clear(); s_resourceHashSetPool.ReturnAndSetNull(ref collector); base.DeserializeChangesCore(reader, committedAt); } + private static void CollectResources(PooledInlineList items, IRenderDataServerResourcesCollector collector) + { + foreach (var item in items) + { + if (item is IRenderDataItemWithServerResources resourceItem) + resourceItem.Collect(collector); + else if (item is RenderDataPushNode pushNode) + CollectResources(pushNode.Children, collector); + } + } + public Rect? Bounds { get @@ -133,4 +142,4 @@ class ServerCompositionRenderData : SimpleServerRenderResource Reset(); base.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs index da24b3812a..f58403f0bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs @@ -31,12 +31,12 @@ internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisp throw new PlatformGraphicsContextLostException(); // This should never happen, but check for it anyway to avoid a deadlock - if (!image.ImportCompeted.IsCompleted) + if (!image.ImportCompleted.IsCompleted) throw new InvalidOperationException("The import operation is not completed yet"); // Rethrow the import here exception - if (image.ImportCompeted.IsFaulted) - image.ImportCompeted.GetAwaiter().GetResult(); + if (image.ImportCompleted.IsFaulted) + image.ImportCompleted.GetAwaiter().GetResult(); } void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context) diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index e0563876bf..1efaa01442 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -99,14 +99,15 @@ namespace Avalonia.Threading } } - public static RestoreContext Ensure(DispatcherPriority priority) + public static RestoreContext Ensure(DispatcherPriority priority) => Ensure(Dispatcher.UIThread, priority); + public static RestoreContext Ensure(Dispatcher dispatcher, DispatcherPriority priority) { if (Current is AvaloniaSynchronizationContext avaloniaContext && avaloniaContext.Priority == priority) return default; var oldContext = Current; - Dispatcher.UIThread.VerifyAccess(); - SetSynchronizationContext(Dispatcher.UIThread.GetContextWithPriority(priority)); + dispatcher.VerifyAccess(); + SetSynchronizationContext(dispatcher.GetContextWithPriority(priority)); return new RestoreContext(oldContext); } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 7d586e1cbd..add990bd57 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -90,7 +90,7 @@ public partial class Dispatcher { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } DispatcherPriority.Validate(priority, "priority"); @@ -98,7 +98,7 @@ public partial class Dispatcher if (timeout.TotalMilliseconds < 0 && timeout != TimeSpan.FromMilliseconds(-1)) { - throw new ArgumentOutOfRangeException("timeout"); + throw new ArgumentOutOfRangeException(nameof(timeout)); } // Fast-Path: if on the same thread, and invoking at Send priority, @@ -106,7 +106,7 @@ public partial class Dispatcher // call the callback directly. if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) { - using (AvaloniaSynchronizationContext.Ensure(priority)) + using (AvaloniaSynchronizationContext.Ensure(this, priority)) callback(); return; } @@ -212,7 +212,7 @@ public partial class Dispatcher { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } DispatcherPriority.Validate(priority, "priority"); @@ -220,7 +220,7 @@ public partial class Dispatcher if (timeout.TotalMilliseconds < 0 && timeout != TimeSpan.FromMilliseconds(-1)) { - throw new ArgumentOutOfRangeException("timeout"); + throw new ArgumentOutOfRangeException(nameof(timeout)); } // Fast-Path: if on the same thread, and invoking at Send priority, @@ -228,7 +228,7 @@ public partial class Dispatcher // call the callback directly. if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) { - using (AvaloniaSynchronizationContext.Ensure(priority)) + using (AvaloniaSynchronizationContext.Ensure(this, priority)) return callback(); } @@ -304,7 +304,7 @@ public partial class Dispatcher { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } DispatcherPriority.Validate(priority, "priority"); @@ -379,7 +379,7 @@ public partial class Dispatcher { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } DispatcherPriority.Validate(priority, "priority"); diff --git a/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs b/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs index e1833fef2b..4b60ee7479 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs @@ -49,7 +49,7 @@ public partial class Dispatcher try { _frames.Push(frame); - using (AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Normal)) + using (AvaloniaSynchronizationContext.Ensure(this, DispatcherPriority.Normal)) frame.Run(_controlledImpl); } finally diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 829ab4cf87..1644332aea 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -9,7 +9,9 @@ public partial class Dispatcher private readonly DispatcherPriorityQueue _queue = new(); private bool _signaled; private bool _explicitBackgroundProcessingRequested; - private const int MaximumTimeProcessingBackgroundJobs = 50; + private const int MaximumInputStarvationTimeInFallbackMode = 50; + private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; + private int _maximumInputStarvationTime; void RequestBackgroundProcessing() { @@ -35,8 +37,8 @@ public partial class Dispatcher lock (InstanceLock) { _explicitBackgroundProcessingRequested = false; - ExecuteJobsCore(); } + ExecuteJobsCore(true); } /// @@ -130,10 +132,10 @@ public partial class Dispatcher lock (InstanceLock) _signaled = false; - ExecuteJobsCore(); + ExecuteJobsCore(false); } - void ExecuteJobsCore() + void ExecuteJobsCore(bool fromExplicitBackgroundProcessingCallback) { long? backgroundJobExecutionStartedAt = null; while (true) @@ -151,7 +153,6 @@ public partial class Dispatcher if (job.Priority > DispatcherPriority.Input) { ExecuteJob(job); - backgroundJobExecutionStartedAt = null; } // If platform supports pending input query, ask the platform if we can continue running low priority jobs else if (_pendingInputImpl?.CanQueryPendingInput == true) @@ -164,6 +165,13 @@ public partial class Dispatcher return; } } + // We can't ask if the implementation has pending input, so we should let it to call us back + // Once it thinks that input is handled + else if (_backgroundProcessingImpl != null && !fromExplicitBackgroundProcessingCallback) + { + RequestBackgroundProcessing(); + return; + } // We can't check if there is pending input, but still need to enforce interactivity // so we stop processing background jobs after some timeout and start a timer to continue later else @@ -171,7 +179,7 @@ public partial class Dispatcher if (backgroundJobExecutionStartedAt == null) backgroundJobExecutionStartedAt = Now; - if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) + if (Now - backgroundJobExecutionStartedAt.Value > _maximumInputStarvationTime) { _signaled = true; RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index bb252b7f55..51408daad1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -127,7 +127,7 @@ public partial class Dispatcher if (needToPromoteTimers) PromoteTimers(); if (needToProcessQueue) - ExecuteJobsCore(); + ExecuteJobsCore(false); UpdateOSTimer(); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index f257072dc8..46c6699b5a 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -34,6 +34,9 @@ public partial class Dispatcher : IDispatcher _controlledImpl = _impl as IControlledDispatcherImpl; _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + _maximumInputStarvationTime = _backgroundProcessingImpl == null ? + MaximumInputStarvationTimeInFallbackMode : + MaximumInputStarvationTimeInExplicitProcessingExplicitMode; if (_backgroundProcessingImpl != null) _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; } diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs index e826432475..1a80740420 100644 --- a/src/Avalonia.Base/Threading/DispatcherFrame.cs +++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs @@ -38,10 +38,14 @@ public class DispatcherFrame /// for their important criteria to be met. These frames /// should have a timeout associated with them. /// - public DispatcherFrame(bool exitWhenRequested) + public DispatcherFrame(bool exitWhenRequested) : this(Dispatcher.UIThread, exitWhenRequested) { - Dispatcher = Dispatcher.UIThread; Dispatcher.VerifyAccess(); + } + + internal DispatcherFrame(Dispatcher dispatcher, bool exitWhenRequested) + { + Dispatcher = dispatcher; _exitWhenRequested = exitWhenRequested; _continue = true; } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 8bd6d3bc01..0008d771c6 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -258,7 +258,7 @@ public class DispatcherOperation try { - using (AvaloniaSynchronizationContext.Ensure(Priority)) + using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority)) InvokeCore(); } finally diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 879d9d8a5f..fbdeed4edc 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -62,7 +62,7 @@ public partial class DispatcherTimer { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } Tick += callback; @@ -112,11 +112,11 @@ public partial class DispatcherTimer bool updateOSTimer = false; if (value.TotalMilliseconds < 0) - throw new ArgumentOutOfRangeException("value", + throw new ArgumentOutOfRangeException(nameof(value), "TimeSpan period must be greater than or equal to zero."); if (value.TotalMilliseconds > Int32.MaxValue) - throw new ArgumentOutOfRangeException("value", + throw new ArgumentOutOfRangeException(nameof(value), "TimeSpan period must be less than or equal to Int32.MaxValue."); lock (_instanceLock) @@ -253,20 +253,20 @@ public partial class DispatcherTimer { if (dispatcher == null) { - throw new ArgumentNullException("dispatcher"); + throw new ArgumentNullException(nameof(dispatcher)); } DispatcherPriority.Validate(priority, "priority"); if (priority == DispatcherPriority.Inactive) { - throw new ArgumentException("Specified priority is not valid.", "priority"); + throw new ArgumentException("Specified priority is not valid.", nameof(priority)); } if (interval.TotalMilliseconds < 0) - throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero."); + throw new ArgumentOutOfRangeException(nameof(interval), "TimeSpan period must be greater than or equal to zero."); if (interval.TotalMilliseconds > Int32.MaxValue) - throw new ArgumentOutOfRangeException("interval", + throw new ArgumentOutOfRangeException(nameof(interval), "TimeSpan period must be less than or equal to Int32.MaxValue."); @@ -349,4 +349,4 @@ public partial class DispatcherTimer // used by Dispatcher internal long DueTimeInMs { get; private set; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index b6e62411da..1e63d554b4 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -51,7 +51,7 @@ internal struct InlineDictionary : IEnumerable : IEnumerable= Count || index < 0) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } if (IsGrouping) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 5d080f5ef6..b2deb1c26e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -222,7 +222,7 @@ namespace Avalonia.Controls { if (element == null) { - throw new ArgumentNullException("element"); + throw new ArgumentNullException(nameof(element)); } if (element is CheckBox checkBox) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs index e7f9a9a6c4..798228c84c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs @@ -191,7 +191,7 @@ namespace Avalonia.Controls } if (dataGridColumn == null) { - throw new ArgumentNullException("dataGridColumn"); + throw new ArgumentNullException(nameof(dataGridColumn)); } int columnIndexWithFiller = columnIndex; diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs index 4841ddd494..a193352f52 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridLength.cs @@ -499,7 +499,7 @@ namespace Avalonia.Controls { if (destinationType == null) { - throw new ArgumentNullException("destinationType"); + throw new ArgumentNullException(nameof(destinationType)); } if (destinationType != typeof(string)) { diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 756951667b..c19d887230 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -18,7 +18,7 @@ namespace Avalonia.Automation.Peers public ControlAutomationPeer(Control owner) { - Owner = owner ?? throw new ArgumentNullException("owner"); + Owner = owner ?? throw new ArgumentNullException(nameof(owner)); Initialize(); } diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs index 835ed1c4af..674eb9a241 100644 --- a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -146,12 +146,12 @@ namespace Avalonia.Automation.Peers if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0)) { - throw new ArgumentOutOfRangeException("horizontalPercent"); + throw new ArgumentOutOfRangeException(nameof(horizontalPercent)); } if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0)) { - throw new ArgumentOutOfRangeException("verticalPercent"); + throw new ArgumentOutOfRangeException(nameof(verticalPercent)); } var offset = Owner.Offset; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index bce1a3685f..bd05d4947c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -270,6 +270,7 @@ namespace Avalonia.Controls { if (_popup?.IsInsidePopup(source) == true) { + e.Handled = true; return; } } @@ -516,5 +517,14 @@ namespace Avalonia.Controls } } } + + /// + /// Clears the selection + /// + public void Clear() + { + SelectedItem = null; + SelectedIndex = -1; + } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index 69b0ffe9a6..5a5c3fdf1d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -165,7 +165,7 @@ namespace Avalonia.Controls.Primitives set { if (value > MaximumValue || value < MinimumValue) - throw new ArgumentOutOfRangeException("SelectedValue"); + throw new ArgumentOutOfRangeException(nameof(value)); var sel = CoerceSelected(value); _selectedValue = sel; @@ -195,7 +195,7 @@ namespace Avalonia.Controls.Primitives set { if (value <= 0 || value > _range) - throw new ArgumentOutOfRangeException("Increment"); + throw new ArgumentOutOfRangeException(nameof(value)); _increment = value; UpdateHelperInfo(); var sel = CoerceSelected(SelectedValue); diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index b5be12fa54..30f9d8f380 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -1113,11 +1113,11 @@ namespace Avalonia.Controls } if (value < Minimum) { - throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + throw new ArgumentOutOfRangeException(nameof(value), $"Value must be greater than Minimum value of {Minimum}"); } else if (value > Maximum) { - throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum)); + throw new ArgumentOutOfRangeException(nameof(value), $"Value must be less than Maximum value of {Maximum}"); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index e22d03273a..66ed1a27e2 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1395,10 +1395,8 @@ namespace Avalonia.Controls.Primitives public object Evaluate(object? dataContext) { - dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext)); - // Only update the DataContext if necessary - if (!dataContext.Equals(DataContext)) + if (!Equals(dataContext, DataContext)) DataContext = dataContext; return GetValue(ValueProperty); diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index 4c1e0c2565..7ff02711d6 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -98,14 +98,14 @@ namespace Avalonia.Controls.PullToRefresh if (_scrollViewer.Content == null) { - throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null."); + throw new ArgumentException("Adaptee's content property cannot be null.", nameof(adaptee)); } var content = adaptee.Content as Visual; if (content == null) { - throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual"); + throw new ArgumentException("Adaptee's content property must be a Visual", nameof(adaptee)); } if (content.GetVisualParent() == null) @@ -118,7 +118,7 @@ namespace Avalonia.Controls.PullToRefresh if (content.Parent is not InputElement) { - throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement"); + throw new ArgumentException("Adaptee's content's parent must be a InputElement", nameof(adaptee)); } } @@ -194,12 +194,12 @@ namespace Avalonia.Controls.PullToRefresh var content = _scrollViewer?.Content as Visual; if (content == null) { - throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); + throw new ArgumentException("Adaptee's content property must be a Visual", nameof(_scrollViewer)); } if (content.Parent is not InputElement parent) { - throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); + throw new ArgumentException("Adaptee's content parent must be an InputElement", nameof(_scrollViewer)); } MakeInteractionSource(parent); diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs index 2ea47b442a..0ae1ef0b97 100644 --- a/src/Avalonia.Controls/Utils/StringUtils.cs +++ b/src/Avalonia.Controls/Utils/StringUtils.cs @@ -76,7 +76,7 @@ namespace Avalonia.Controls.Utils } var codepoint = new Codepoint(text[index]); - + if (!codepoint.IsWhiteSpace) { return false; @@ -85,12 +85,20 @@ namespace Avalonia.Controls.Utils // preceeded by lwsp. if (index > 0) { - var nextCodePoint = new Codepoint(text[index + 1]); + if (index + 1 < text.Length) + { + var nextCodePoint = new Codepoint(text[index + 1]); - if (nextCodePoint.IsBreakChar) + if (nextCodePoint.IsBreakChar) + { + return true; + } + } + else { return true; } + } switch (codepoint.GeneralCategory) @@ -125,7 +133,7 @@ namespace Avalonia.Controls.Utils } cursor = Math.Min(cursor, text.Length); - + int begin; int i; int cr; @@ -181,7 +189,7 @@ namespace Avalonia.Controls.Utils { return cursor; } - + if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n') { lf = cr + 1; @@ -214,9 +222,9 @@ namespace Avalonia.Controls.Utils { return i; } - + var cc = GetCharClass(text[i]); - + // skip over the word, punctuation, or run of whitespace while (i < cr && GetCharClass(text[i]) == cc) { diff --git a/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs index f92756c59b..eaf501b45f 100644 --- a/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs +++ b/src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Utils get { if(index < 0 || index >= Count) - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); index += _start; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 37e5e41faf..b7c58cded9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -3,9 +3,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.MicroCom; using Avalonia.Native.Interop; -using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; @@ -163,6 +161,14 @@ namespace Avalonia.Native Compositor = new Compositor(_platformGraphics, true); + + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + + private void OnProcessExit(object? sender, EventArgs e) + { + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; + _factory.Dispose(); } public ITrayIconImpl CreateTrayIcon() diff --git a/src/Avalonia.Native/NativePlatformSettings.cs b/src/Avalonia.Native/NativePlatformSettings.cs index 53d3da0378..42d05d831c 100644 --- a/src/Avalonia.Native/NativePlatformSettings.cs +++ b/src/Avalonia.Native/NativePlatformSettings.cs @@ -23,7 +23,7 @@ internal class NativePlatformSettings : DefaultPlatformSettings AvnPlatformThemeVariant.Dark => (PlatformThemeVariant.Dark, ColorContrastPreference.NoPreference), AvnPlatformThemeVariant.Light => (PlatformThemeVariant.Light, ColorContrastPreference.NoPreference), AvnPlatformThemeVariant.HighContrastDark => (PlatformThemeVariant.Dark, ColorContrastPreference.High), - AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Dark, ColorContrastPreference.High), + AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Light, ColorContrastPreference.High), _ => throw new ArgumentOutOfRangeException() }; var color = _platformSettings.AccentColor; diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 8966dd4206..9c5f92b6ac 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -752,7 +752,7 @@ namespace Metsys.Bson if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert) { - throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), nameof(lambdaExpression)); + throw new ArgumentException($"Expression '{lambdaExpression}' must resolve to top-level member.", nameof(lambdaExpression)); } return memberExpression.Member.Name; default: @@ -942,7 +942,7 @@ namespace Metsys.Bson return new ListWrapper(); } } - throw new BsonException(string.Format("Collection of type {0} cannot be deserialized", type.FullName)); + throw new BsonException($"Collection of type {type.FullName} cannot be deserialized"); } public abstract void Add(object value); @@ -1514,7 +1514,7 @@ namespace Metsys.Bson.Configuration result = Visit((MemberExpression)expression.Left); } var index = Expression.Lambda(expression.Right).Compile().DynamicInvoke(); - return result + string.Format("[{0}]", index); + return result + $"[{index}]"; } private string Visit(MemberExpression expression) @@ -1540,7 +1540,7 @@ namespace Metsys.Bson.Configuration if (expression.Method.Name == "get_Item" && expression.Arguments.Count == 1) { var index = Expression.Lambda(expression.Arguments[0]).Compile().DynamicInvoke(); - name += string.Format("[{0}]", index); + name += $"[{index}]"; } return name; } diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index 6e18151fae..637d44d617 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -14,6 +14,7 @@ namespace Avalonia.X11 private readonly X11Info _x11; private IDataObject _storedDataObject; private IntPtr _handle; + private TaskCompletionSource _storeAtomTcs; private TaskCompletionSource _requestedFormatsTcs; private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; @@ -52,6 +53,12 @@ namespace Avalonia.X11 private unsafe void OnEvent(ref XEvent ev) { + if (ev.type == XEventName.SelectionClear) + { + _storeAtomTcs?.TrySetResult(true); + return; + } + if (ev.type == XEventName.SelectionRequest) { var sel = ev.SelectionRequestEvent; @@ -82,18 +89,9 @@ namespace Avalonia.X11 Encoding textEnc; if (target == _x11.Atoms.TARGETS) { - var atoms = new HashSet { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; - foreach (var fmt in _storedDataObject.GetDataFormats()) - { - if (fmt == DataFormats.Text) - foreach (var ta in _textAtoms) - atoms.Add(ta); - else - atoms.Add(_x11.Atoms.GetAtom(fmt)); - } - + var atoms = ConvertDataObject(_storedDataObject); XChangeProperty(_x11.Display, window, property, - _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms.ToArray(), atoms.Count); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) @@ -252,20 +250,41 @@ namespace Avalonia.X11 return (string)await SendDataRequest(target); } - private void StoreAtomsInClipboardManager(IntPtr[] atoms) + + private IntPtr[] ConvertDataObject(IDataObject data) + { + var atoms = new HashSet { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; + foreach (var fmt in data.GetDataFormats()) + { + if (fmt == DataFormats.Text) + foreach (var ta in _textAtoms) + atoms.Add(ta); + else + atoms.Add(_x11.Atoms.GetAtom(fmt)); + } + return atoms.ToArray(); + } + + private Task StoreAtomsInClipboardManager(IDataObject data) { if (_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) { var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER); if (clipboardManager != IntPtr.Zero) - { + { + if (_storeAtomTcs == null || _storeAtomTcs.Task.IsCompleted) + _storeAtomTcs = new TaskCompletionSource(); + + var atoms = ConvertDataObject(data); XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS, _avaloniaSaveTargetsAtom, _handle, IntPtr.Zero); + return _storeAtomTcs.Task; } } + return Task.CompletedTask; } public Task SetTextAsync(string text) @@ -283,9 +302,8 @@ namespace Avalonia.X11 public Task SetDataObjectAsync(IDataObject data) { _storedDataObject = data; - XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); - StoreAtomsInClipboardManager(_textAtoms); - return Task.CompletedTask; + XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); + return StoreAtomsInClipboardManager(data); } public async Task GetFormatsAsync() diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 18f860a1a8..86ef7879a5 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1109,7 +1109,7 @@ namespace Avalonia.X11 { public override string ToString () { - return string.Format("MotifWmHints peer.GetPeerFromPoint(p)); + var found = InvokeSync(() => Peer.GetPeerFromPoint(p)); var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs index bef5a55b06..98e1077885 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs @@ -28,6 +28,9 @@ namespace Avalonia.Win32.WinRT.Composition public static readonly Guid CLSID_D2D1Border = new Guid(0x2A2D49C0, 0x4ACF, 0x43C7, 0x8C, 0x6A, 0x7C, 0x4A, 0x27, 0x87, 0x4D, 0x27); + public static readonly Guid CLSID_D2D1Opacity = + new Guid("811d79a4-de28-4454-8094-c64685f8bd4c"); + public static readonly Guid CLSID_D2D1Brightness = new Guid(0x8CEA8D1E, 0x77B0, 0x4986, 0xB3, 0xB9, 0x2F, 0x0C, 0x0E, 0xAE, 0x78, 0x87); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs index 1a922b4acd..10dbcfcc6b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs @@ -52,6 +52,110 @@ namespace Avalonia.Win32.WinRT.Composition _sources = null; } } + + class BorderEffect : WinUIEffectBase + { + private readonly int _x; + private readonly int _y; + public override Guid EffectId => D2DEffects.CLSID_D2D1Border; + public override uint PropertyCount => 2; + + public BorderEffect(int x, int y, params IGraphicsEffectSource[] _sources):base(_sources) + { + _x = x; + _y = y; + } + + public override IPropertyValue? GetProperty(uint index) + { + if (index == 0) + return new WinRTPropertyValue((uint)_x); + if (index == 1) + return new WinRTPropertyValue((uint)_y); + return null; + } + } + + class BlendEffect : WinUIEffectBase + { + private readonly int _mode; + + public BlendEffect(int mode, params IGraphicsEffectSource[] _sources) : base(_sources) + { + _mode = mode; + } + + public override Guid EffectId => D2DEffects.CLSID_D2D1Blend; + public override uint PropertyCount => 1; + + public override IPropertyValue? GetProperty(uint index) + { + if (index == 0) + return new WinRTPropertyValue((uint)_mode); + return null; + } + } + + class CompositeStepEffect : WinUIEffectBase + { + private readonly float _mode; + + public CompositeStepEffect(int mode, params IGraphicsEffectSource[] _sources) : base(_sources) + { + _mode = mode; + } + + public override Guid EffectId => D2DEffects.CLSID_D2D1Composite; + public override uint PropertyCount => 1; + + public override IPropertyValue? GetProperty(uint index) + { + if (index == 0) + return new WinRTPropertyValue((uint)_mode); + return null; + } + } + + class OpacityEffect : WinUIEffectBase + { + private readonly float _opacity; + + public OpacityEffect(float opacity, params IGraphicsEffectSource[] _sources) : base(_sources) + { + _opacity = opacity; + } + + public override Guid EffectId => D2DEffects.CLSID_D2D1Opacity; + public override uint PropertyCount => 1; + + public override IPropertyValue? GetProperty(uint index) + { + if (index == 0) + return new WinRTPropertyValue(_opacity); + return null; + } + } + + class ColorSourceEffect : WinUIEffectBase + { + private readonly float[] _color; + + public ColorSourceEffect(float[] color) + { + _color = color; + } + + public override Guid EffectId => D2DEffects.CLSID_D2D1Flood; + public override uint PropertyCount => 1; + + public override IPropertyValue? GetProperty(uint index) + { + if (index == 0) + return new WinRTPropertyValue(_color); + return null; + } + } + internal class WinUIGaussianBlurEffect : WinUIEffectBase { diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs index 2f22ba99f9..d720e525d3 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -12,7 +12,8 @@ internal class WinUiCompositedWindow : IDisposable public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; } private readonly WinUiCompositionShared _shared; private readonly ICompositionRoundedRectangleGeometry? _compositionRoundedRectangleGeometry; - private readonly IVisual? _mica; + private readonly IVisual? _micaLight; + private readonly IVisual? _micaDark; private readonly IVisual _blur; private readonly IVisual _visual; private PixelSize _size; @@ -25,7 +26,8 @@ internal class WinUiCompositedWindow : IDisposable { _compositionRoundedRectangleGeometry?.Dispose(); _blur.Dispose(); - _mica?.Dispose(); + _micaLight?.Dispose(); + _micaDark?.Dispose(); _visual.Dispose(); _surfaceBrush.Dispose(); _target.Dispose(); @@ -50,14 +52,20 @@ internal class WinUiCompositedWindow : IDisposable _target.SetRoot(containerVisual); _blur = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.BlurBrush); - if (shared.MicaBrush != null) + if (shared.MicaBrushLight != null) { - _mica = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrush); - containerChildren.InsertAtTop(_mica); + _micaLight = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrushLight); + containerChildren.InsertAtTop(_micaLight); + } + + if (shared.MicaBrushDark != null) + { + _micaDark = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrushDark); + containerChildren.InsertAtTop(_micaDark); } _compositionRoundedRectangleGeometry = - WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _mica); + WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _micaLight, _micaDark); containerChildren.InsertAtTop(_blur); using var spriteVisual = shared.Compositor.CreateSpriteVisual(); @@ -68,9 +76,6 @@ internal class WinUiCompositedWindow : IDisposable using var compositionBrush = _surfaceBrush.QueryInterface(); spriteVisual.SetBrush(compositionBrush); _target.SetRoot(containerVisual); - - - } public void SetSurface(ICompositionSurface surface) => _surfaceBrush.SetSurface(surface); @@ -79,12 +84,13 @@ internal class WinUiCompositedWindow : IDisposable { lock (_shared.SyncRoot) { - _blur.SetIsVisible(blurEffect == BlurEffect.Acrylic - || blurEffect == BlurEffect.Mica && _mica == null ? + || (blurEffect == BlurEffect.MicaLight && _micaLight == null) || + (blurEffect == BlurEffect.MicaDark && _micaDark == null) ? 1 : 0); - _mica?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + _micaLight?.SetIsVisible(blurEffect == BlurEffect.MicaLight ? 1 : 0); + _micaDark?.SetIsVisible(blurEffect == BlurEffect.MicaDark ? 1 : 0); } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs index b3a328d097..7a775afb3a 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs @@ -9,7 +9,8 @@ internal class WinUiCompositionShared : IDisposable public ICompositor5 Compositor5 { get; } public ICompositorDesktopInterop DesktopInterop { get; } public ICompositionBrush BlurBrush { get; } - public ICompositionBrush? MicaBrush { get; } + public ICompositionBrush? MicaBrushLight { get; } + public ICompositionBrush? MicaBrushDark { get; } public object SyncRoot { get; } = new(); public static readonly Version MinWinCompositionVersion = new(10, 0, 17134); @@ -21,14 +22,16 @@ internal class WinUiCompositionShared : IDisposable Compositor = compositor.CloneReference(); Compositor5 = compositor.QueryInterface(); BlurBrush = WinUiCompositionUtils.CreateAcrylicBlurBackdropBrush(compositor); - MicaBrush = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor); + MicaBrushLight = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor, 242, 0.6f); + MicaBrushDark = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor, 32, 0.8f); DesktopInterop = compositor.QueryInterface(); } public void Dispose() { BlurBrush.Dispose(); - MicaBrush?.Dispose(); + MicaBrushLight?.Dispose(); + MicaBrushDark?.Dispose(); DesktopInterop.Dispose(); Compositor.Dispose(); Compositor5.Dispose(); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs index 7b970868df..29c1e1fe27 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using MicroCom.Runtime; @@ -5,16 +6,72 @@ namespace Avalonia.Win32.WinRT.Composition; internal static class WinUiCompositionUtils { - public static ICompositionBrush? CreateMicaBackdropBrush(ICompositor compositor) + public static ICompositionBrush? CreateMicaBackdropBrush(ICompositor compositor, float color, float opacity) { if (Win32Platform.WindowsVersion.Build < 22000) return null; + using var backDropParameterFactory = + NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); + + + var tint = new[] { color / 255f, color / 255f, color / 255f, 255f / 255f }; + + using var tintColorEffect = new ColorSourceEffect(tint); + + + using var tintOpacityEffect = new OpacityEffect(1.0f, tintColorEffect); + using var tintOpacityEffectFactory = compositor.CreateEffectFactory(tintOpacityEffect); + using var tintOpacityEffectBrushEffect = tintOpacityEffectFactory.CreateBrush(); + using var tintOpacityEffectBrush = tintOpacityEffectBrushEffect.QueryInterface(); + + using var luminosityColorEffect = new ColorSourceEffect(tint); + + using var luminosityOpacityEffect = new OpacityEffect(opacity, luminosityColorEffect); + using var luminosityOpacityEffectFactory = compositor.CreateEffectFactory(luminosityOpacityEffect); + using var luminosityOpacityEffectBrushEffect = luminosityOpacityEffectFactory.CreateBrush(); + using var luminosityOpacityEffectBrush = + luminosityOpacityEffectBrushEffect.QueryInterface(); + using var compositorWithBlurredWallpaperBackdropBrush = compositor.QueryInterface(); using var blurredWallpaperBackdropBrush = compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); - return blurredWallpaperBackdropBrush?.QueryInterface(); + using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); + + + using var backgroundParameterAsSource = + GetParameterSource("Background", backDropParameterFactory, out var backgroundHandle); + using var foregroundParameterAsSource = + GetParameterSource("Foreground", backDropParameterFactory, out var foregroundHandle); + + using var luminosityBlendEffect = + new BlendEffect(23, backgroundParameterAsSource, foregroundParameterAsSource); + using var luminosityBlendEffectFactory = compositor.CreateEffectFactory(luminosityBlendEffect); + using var luminosityBlendEffectBrush = luminosityBlendEffectFactory.CreateBrush(); + using var luminosityBlendEffectBrush1 = luminosityBlendEffectBrush.QueryInterface(); + luminosityBlendEffectBrush.SetSourceParameter(backgroundHandle, micaBackdropBrush); + luminosityBlendEffectBrush.SetSourceParameter(foregroundHandle, luminosityOpacityEffectBrush); + + + using var backgroundParameterAsSource1 = + GetParameterSource("Background", backDropParameterFactory, out var backgroundHandle1); + using var foregroundParameterAsSource1 = + GetParameterSource("Foreground", backDropParameterFactory, out var foregroundHandle1); + + using var colorBlendEffect = + new BlendEffect(22, backgroundParameterAsSource1, foregroundParameterAsSource1); + using var colorBlendEffectFactory = compositor.CreateEffectFactory(colorBlendEffect); + using var colorBlendEffectBrush = colorBlendEffectFactory.CreateBrush(); + colorBlendEffectBrush.SetSourceParameter(backgroundHandle1, luminosityBlendEffectBrush1); + colorBlendEffectBrush.SetSourceParameter(foregroundHandle1, tintOpacityEffectBrush); + + + // colorBlendEffectBrush.SetSourceParameter(backgroundHandle, micaBackdropBrush); + + using var micaBackdropBrush1 = colorBlendEffectBrush.QueryInterface(); + return micaBackdropBrush1.CloneReference(); } public static ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) @@ -97,4 +154,15 @@ internal static class WinUiCompositionUtils brush?.Dispose(); } } + + private static IGraphicsEffectSource GetParameterSource(string name, + ICompositionEffectSourceParameterFactory backDropParameterFactory, out IntPtr handle) + { + var backdropString = new HStringInterop(name); + var backDropParameter = + backDropParameterFactory.Create(backdropString.Handle); + var backDropParameterAsSource = backDropParameter.QueryInterface(); + handle = backdropString.Handle; + return backDropParameterAsSource; + } } diff --git a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs index a3918d9ae6..5f2e752af8 100644 --- a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs +++ b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs @@ -4,7 +4,8 @@ { None, Acrylic, - Mica + MicaLight, + MicaDark } internal interface IBlurHost diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs index 684e7ff7b5..8874902d4e 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Avalonia.Win32.WinRT @@ -16,7 +17,15 @@ namespace Avalonia.Win32.WinRT UInt32 = u; Type = PropertyType.UInt32; } - + + public WinRTPropertyValue(float[] uiColor) + { + Type = PropertyType.SingleArray; + _singleArray = uiColor; + } + + private readonly float[]? _singleArray; + public PropertyType Type { get; } public int IsNumericScalar { get; } public byte UInt8 { get; } @@ -62,7 +71,17 @@ namespace Avalonia.Win32.WinRT public unsafe ulong* GetUInt64Array(uint* __valueSize) => throw NotImplemented; - public unsafe float* GetSingleArray(uint* __valueSize) => throw NotImplemented; + public unsafe float* GetSingleArray(uint* __valueSize) + { + if (_singleArray == null) + throw NotImplemented; + *__valueSize = (uint)_singleArray.Length; + var allocCoTaskMem = Marshal.AllocCoTaskMem(_singleArray.Length * Unsafe.SizeOf()); + Marshal.Copy(_singleArray, 0, allocCoTaskMem, _singleArray.Length); + float* s = (float*)allocCoTaskMem; + + return s; + } public unsafe double* GetDoubleArray(uint* __valueSize) => throw NotImplemented; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 057cdb2db0..044c2cad67 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -106,6 +106,7 @@ namespace Avalonia.Win32 private static POINTER_PEN_INFO[]? s_historyPenInfos; private static POINTER_INFO[]? s_historyInfos; private static MOUSEMOVEPOINT[]? s_mouseHistoryInfos; + private PlatformThemeVariant _currentThemeVariant; public WindowImpl() { @@ -474,7 +475,12 @@ namespace Avalonia.Win32 return; SetUseHostBackdropBrush(false); - _blurHost?.SetBlur(BlurEffect.Mica); + _blurHost?.SetBlur(_currentThemeVariant switch + { + PlatformThemeVariant.Light => BlurEffect.MicaLight, + PlatformThemeVariant.Dark => BlurEffect.MicaDark, + _ => throw new ArgumentOutOfRangeException() + }); } private void SetAccentState(AccentState state) @@ -778,6 +784,7 @@ namespace Avalonia.Win32 public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { + _currentThemeVariant = themeVariant; if (Win32Platform.WindowsVersion.Build >= 22000) { var pvUseBackdropBrush = themeVariant == PlatformThemeVariant.Dark ? 1 : 0; @@ -786,6 +793,10 @@ namespace Avalonia.Win32 (int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, &pvUseBackdropBrush, sizeof(int)); + if (TransparencyLevel == WindowTransparencyLevel.Mica) + { + SetTransparencyMica(Win32Platform.WindowsVersion); + } } } diff --git a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj index c27801db61..05d72618a9 100644 --- a/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj +++ b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj @@ -5,6 +5,7 @@ Avalonia.Analyzers true true + false true diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj index cd1e2fcaba..c6992f9438 100644 --- a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj @@ -5,6 +5,7 @@ Avalonia.Generators $(DefineConstants);XAMLX_INTERNAL true + false true true diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index c273cc6489..69d7fc4916 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { - var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); + var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex + currentLength)); Assert.Equal(previousIndex, characterHit.FirstCharacterIndex); diff --git a/tests/Avalonia.Base.UnitTests/Utilities/InlineDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/InlineDictionaryTests.cs new file mode 100644 index 0000000000..af47d2726a --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/InlineDictionaryTests.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System.Collections.Generic; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities; + +public class InlineDictionaryTests +{ + [Fact] + public void Enumeration_After_Add_With_Internal_Array_Works() + { + var dic = new InlineDictionary(); + dic.Add("foo", 1); + dic.Add("bar", 2); + dic.Add("baz", 3); + + Assert.Equal( + new[] { + new KeyValuePair("foo", 1), + new KeyValuePair("bar", 2), + new KeyValuePair("baz", 3) + }, + dic); + } + + [Fact] + public void Enumeration_After_Remove_With_Internal_Array_Works() + { + var dic = new InlineDictionary(); + dic.Add("foo", 1); + dic.Add("bar", 2); + dic.Add("baz", 3); + + Assert.Equal( + new[] { + new KeyValuePair("foo", 1), + new KeyValuePair("bar", 2), + new KeyValuePair("baz", 3) + }, + dic); + + dic.Remove("bar"); + + Assert.Equal( + new[] { + new KeyValuePair("foo", 1), + new KeyValuePair("baz", 3) + }, + dic); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs index e9b8895174..9ea12c62fb 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs @@ -268,6 +268,22 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(called); } + [Fact] + public void Handles_Null_SelectedItem_When_SelectedValueBinding_Assigned() + { + // Issue #11220 + var items = new object[] { null }; + var sic = new SelectingItemsControl + { + ItemsSource = items, + SelectedIndex = 0, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + Assert.Null(sic.SelectedValue); + } + private static FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 12427e1f9e..d576a64523 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -194,7 +194,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting for (var i = 0; i < clusters.Count; i++) { var expectedCluster = clusters[i]; - var actualCluster = nextCharacterHit.FirstCharacterIndex; + var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(expectedCluster, actualCluster); @@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); } - - firstCharacterHit = previousCharacterHit; - - firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit); - - previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit); - - Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex); - - Assert.Equal(0, previousCharacterHit.TrailingLength); } } @@ -728,6 +718,110 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new MixedTextBufferTextSource(); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1)); + + Assert.Equal(10, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(characterHit); + + Assert.Equal(11, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1)); + + Assert.Equal(20, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10)); + + Assert.Equal(11, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(characterHit); + + Assert.Equal(12, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20)); + + Assert.Equal(21, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + } + } + + [Fact] + public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new MixedTextBufferTextSource(); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1)); + + Assert.Equal(19, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1)); + + Assert.Equal(9, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); + + Assert.Equal(8, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21)); + + Assert.Equal(20, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11)); + + Assert.Equal(10, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); + + Assert.Equal(9, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + } + } + private class MixedTextBufferTextSource : ITextSource { public TextRun? GetTextRun(int textSourceIndex)