瀏覽代碼

Merge branch 'master' into fixes/refactoring/unused_namespace

Max Katz 3 年之前
父節點
當前提交
6d99408461
共有 46 個文件被更改,包括 1058 次插入494 次删除
  1. 3 3
      azure-pipelines.yml
  2. 19 7
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 29 4
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  4. 1 1
      src/Avalonia.Base/Media/GlyphRun.cs
  5. 24 0
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  6. 12 5
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  7. 13 13
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  8. 6 1
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  9. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextLine.cs
  10. 324 78
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  11. 1 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  12. 4 4
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  13. 14 2
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  14. 1 1
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  15. 2 2
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  16. 10 1
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  17. 1 1
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  18. 2 2
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
  19. 1 1
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
  20. 2 2
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
  21. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  22. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  23. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  24. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  25. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  26. 1 1
      src/Avalonia.Base/Utilities/MathUtilities.cs
  27. 11 12
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  28. 174 172
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  29. 48 46
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  30. 41 35
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  31. 8 12
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  32. 3 1
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  33. 38 18
      src/Avalonia.Controls/Button.cs
  34. 41 16
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  35. 4 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  36. 1 1
      src/Avalonia.Controls/Primitives/Popup.cs
  37. 12 12
      src/Avalonia.Controls/RichTextBlock.cs
  38. 4 2
      src/Avalonia.Controls/TextBox.cs
  39. 29 4
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  40. 31 14
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts
  41. 21 4
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  42. 44 0
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  43. 二進制
      tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf
  44. 二進制
      tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf
  45. 7 1
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  46. 65 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

+ 3 - 3
azure-pipelines.yml

@@ -59,7 +59,7 @@ jobs:
   variables:
     SolutionDir: '$(Build.SourcesDirectory)'
   pool:
-    vmImage: 'macOS-10.15'
+    vmImage: 'macos-12'
   steps:
   - task: UseDotNet@2
     displayName: 'Use .NET Core SDK 3.1.418'
@@ -91,10 +91,10 @@ jobs:
     inputs:
       actions: 'build'
       scheme: ''
-      sdk: 'macosx11.1'
+      sdk: 'macosx12.3'
       configuration: 'Release'
       xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
-      xcodeVersion: '12' # Options: 8, 9, default, specifyPath
+      xcodeVersion: '13' # Options: 8, 9, default, specifyPath
       args: '-derivedDataPath ./'
 
   - task: CmdLine@2

+ 19 - 7
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -195,10 +195,10 @@ namespace ControlCatalog.Pages
                 {
                     // Sync disposal of StreamWriter is not supported on WASM
 #if NET6_0_OR_GREATER
-                    await using var stream = await file.OpenWrite();
+                    await using var stream = await file.OpenWriteAsync();
                     await using var reader = new System.IO.StreamWriter(stream);
 #else
-                    using var stream = await file.OpenWrite();
+                    using var stream = await file.OpenWriteAsync();
                     using var reader = new System.IO.StreamWriter(stream);
 #endif
                     await reader.WriteLineAsync(openedFileContent.Text);
@@ -243,8 +243,8 @@ namespace ControlCatalog.Pages
             async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
             {
                 items ??= Array.Empty<IStorageItem>();
-                var mappedResults = items.Select(FullPathOrName).ToList();
-                bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark";
+                bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark";
+                var mappedResults = new List<string>();
 
                 if (items.FirstOrDefault() is IStorageItem item)
                 {
@@ -267,9 +267,9 @@ Content:
                         if (file.CanOpenRead)
                         {
 #if NET6_0_OR_GREATER
-                            await using var stream = await file.OpenRead();
+                            await using var stream = await file.OpenReadAsync();
 #else
-                            using var stream = await file.OpenRead();
+                            using var stream = await file.OpenReadAsync();
 #endif
                             using var reader = new System.IO.StreamReader(stream);
 
@@ -293,7 +293,19 @@ Content:
                     lastSelectedDirectory = await item.GetParentAsync();
                     if (lastSelectedDirectory is not null)
                     {
-                        mappedResults.Insert(0,  "Parent: " + FullPathOrName(lastSelectedDirectory));
+                        mappedResults.Add(FullPathOrName(lastSelectedDirectory));
+                    }
+
+                    foreach (var selectedItem in items)
+                    {
+                        mappedResults.Add("+> " + FullPathOrName(selectedItem));
+                        if (selectedItem is IStorageFolder folder)
+                        {
+                            foreach (var innerItems in await folder.GetItemsAsync())
+                            {
+                                mappedResults.Add("++> " + FullPathOrName(innerItems));
+                            }
+                        }
                     }
                 }
 

+ 29 - 4
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@@ -1,6 +1,7 @@
 #nullable enable
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
@@ -35,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
 
     public bool CanBookmark => true;
 
-    public Task<string?> SaveBookmark()
+    public Task<string?> SaveBookmarkAsync()
     {
         Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
         return Task.FromResult(Uri.ToString());
     }
 
-    public Task ReleaseBookmark()
+    public Task ReleaseBookmarkAsync()
     {
         Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
         return Task.CompletedTask;
@@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
     {
         return Task.FromResult(new StorageItemProperties());
     }
+
+    public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    {
+        using var javaFile = new JavaFile(Uri.Path!);
+
+        // Java file represents files AND directories. Don't be confused.
+        var files = await javaFile.ListFilesAsync().ConfigureAwait(false);
+        if (files is null)
+        {
+            return Array.Empty<IStorageItem>();
+        }
+
+        return files
+            .Select(f => (file: f, uri: AndroidUri.FromFile(f)))
+            .Where(t => t.uri is not null)
+            .Select(t => t.file switch
+            {
+                { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!),
+                { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!),
+                _ => null
+            })
+            .Where(i => i is not null)
+            .ToArray()!;
+    }
 }
 
 internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
@@ -118,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
 
     public bool CanOpenWrite => true;
 
-    public Task<Stream> OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false)
+    public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false)
         ?? throw new InvalidOperationException("Failed to open content stream"));
 
-    public Task<Stream> OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true)
+    public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true)
         ?? throw new InvalidOperationException("Failed to open content stream"));
 
     private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)

+ 1 - 1
src/Avalonia.Base/Media/GlyphRun.cs

@@ -265,7 +265,7 @@ namespace Avalonia.Media
                 //RightToLeft
                 var glyphIndex = FindGlyphIndex(characterIndex);
 
-                if (GlyphClusters != null)
+                if (GlyphClusters != null && GlyphClusters.Count > 0)
                 {
                     if (characterIndex > GlyphClusters[0])
                     {

+ 24 - 0
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
@@ -116,7 +117,30 @@ namespace Avalonia.Media.TextFormatting
                 length = text.Length;
             }
 
+            length = CoerceLength(text, length);
+
             return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
         }
+
+        private static int CoerceLength(ReadOnlySlice<char> text, int length)
+        {
+            var finalLength = 0;
+
+            var graphemeEnumerator = new GraphemeEnumerator(text);
+
+            while (graphemeEnumerator.MoveNext())
+            {
+                var grapheme = graphemeEnumerator.Current;
+
+                finalLength += grapheme.Text.Length;
+
+                if (finalLength >= length)
+                {
+                    return finalLength;
+                }
+            }
+
+            return length;
+        }
     }
 }

+ 12 - 5
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -15,6 +15,13 @@ namespace Avalonia.Media.TextFormatting
 
         public override void Justify(TextLine textLine)
         {
+            var lineImpl = textLine as TextLineImpl;
+
+            if(lineImpl is null)
+            {
+                return;
+            }
+
             var paragraphWidth = Width;
 
             if (double.IsInfinity(paragraphWidth))
@@ -22,12 +29,12 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            if (textLine.NewLineLength > 0)
+            if (lineImpl.NewLineLength > 0)
             {
                 return;
             }
 
-            var textLineBreak = textLine.TextLineBreak;
+            var textLineBreak = lineImpl.TextLineBreak;
 
             if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null)
             {
@@ -39,7 +46,7 @@ namespace Avalonia.Media.TextFormatting
 
             var breakOportunities = new Queue<int>();
 
-            foreach (var textRun in textLine.TextRuns)
+            foreach (var textRun in lineImpl.TextRuns)
             {
                 var text = textRun.Text;
 
@@ -68,10 +75,10 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace);
+            var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
             var spacing = remainingSpace / breakOportunities.Count;
 
-            foreach (var textRun in textLine.TextRuns)
+            foreach (var textRun in lineImpl.TextRuns)
             {
                 var text = textRun.Text;
 

+ 13 - 13
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="ShapeableTextCharacters"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel, 
+        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
             ref TextRunProperties? previousProperties)
         {
             var shapeableCharacters = new List<ShapeableTextCharacters>(2);
@@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="biDiLevel">The bidi level of the run.</param>
         /// <param name="previousProperties"></param>
         /// <returns>A list of shapeable text runs.</returns>
-        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, 
+        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
@@ -76,7 +76,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 if (script == Script.Common && previousTypeface is not null)
                 {
-                    if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
+                    if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
                     {
                         return new ShapeableTextCharacters(text.Take(fallbackCount),
                             defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -86,10 +86,10 @@ namespace Avalonia.Media.TextFormatting
                 return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
-            
+
             if (previousTypeface is not null)
             {
-                if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
+                if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
                 {
                     return new ShapeableTextCharacters(text.Take(count),
                         defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting
                 {
                     continue;
                 }
-                
+
                 codepoint = codepointEnumerator.Current;
-                    
+
                 break;
             }
-            
+
             //ToDo: Fix FontFamily fallback
             var matchFound =
                 FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
@@ -157,14 +157,14 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="script"></param>
         /// <returns></returns>
         protected static bool TryGetShapeableLength(
-            ReadOnlySlice<char> text, 
-            Typeface typeface, 
+            ReadOnlySlice<char> text,
+            Typeface typeface,
             Typeface? defaultTypeface,
             out int length,
             out Script script)
         {
             length = 0;
-            script = Script.Unknown;         
+            script = Script.Unknown;
 
             if (text.Length == 0)
             {
@@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var currentScript = currentGrapheme.FirstCodepoint.Script;
 
-                if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+                if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                 {
                     break;
                 }
@@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     break;
                 }
-                
+
                 if (currentScript != script)
                 {
                     if (script is Script.Unknown || currentScript != Script.Common &&

+ 6 - 1
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <param name="width">The collapsing width.</param>
         /// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
-        private TextCollapsingProperties GetCollapsingProperties(double width)
+        private TextCollapsingProperties? GetCollapsingProperties(double width)
         {
+            if(_textTrimming == TextTrimming.None)
+            {
+                return null;
+            }
+
             return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
         }
     }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

@@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// A <see cref="TextLine"/> value that represents a collapsed line that can be displayed.
         /// </returns>
-        public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
+        public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList);
 
         /// <summary>
         /// Create a justified line based on justification text properties.

+ 324 - 78
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <inheritdoc/>
-        public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
+        public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList)
         {
             if (collapsingPropertiesList.Length == 0)
             {
@@ -128,6 +128,11 @@ namespace Avalonia.Media.TextFormatting
 
             var collapsingProperties = collapsingPropertiesList[0];
 
+            if(collapsingProperties is null)
+            {
+                return this;
+            }
+
             var collapsedRuns = collapsingProperties.Collapse(this);
 
             if (collapsedRuns is null)
@@ -171,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
                 return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
             }
 
-            if (distance > WidthIncludingTrailingWhitespace)
+            if (distance >= WidthIncludingTrailingWhitespace)
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
@@ -183,8 +188,52 @@ namespace Avalonia.Media.TextFormatting
             var currentPosition = FirstTextSourceIndex;
             var currentDistance = 0.0;
 
-            foreach (var currentRun in _textRuns)
+            for (var i = 0; i < _textRuns.Count; i++)
             {
+                var currentRun = _textRuns[i];
+
+                if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                {
+                    var rightToLeftIndex = i;
+                    currentPosition += currentRun.TextSourceLength;
+
+                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    {
+                        var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
+
+                        if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
+                        {
+                            break;
+                        }
+
+                        currentPosition += nextShaped.TextSourceLength;
+
+                        rightToLeftIndex++;
+                    }
+
+                    for (var j = i; i <= rightToLeftIndex; j++)
+                    {
+                        if(j > _textRuns.Count - 1)
+                        {
+                            break;
+                        }
+
+                        currentRun = _textRuns[j];
+
+                        if(currentDistance + currentRun.Size.Width <= distance)
+                        {
+                            currentDistance += currentRun.Size.Width;
+                            currentPosition -= currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
+
+                        break;
+                    }
+                }
+
                 if (currentDistance + currentRun.Size.Width < distance)
                 {
                     currentDistance += currentRun.Size.Width;
@@ -211,12 +260,16 @@ namespace Avalonia.Media.TextFormatting
                     {
                         characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
 
-                        var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+                        var offset = 0;
 
-                        if (!shapedRun.GlyphRun.IsLeftToRight)
+                        if (shapedRun.GlyphRun.IsLeftToRight)
                         {
-                            offset = Math.Max(0, offset - shapedRun.Text.End);
+                            offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
                         }
+                        //else
+                        //{
+                        //    offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length);
+                        //}
 
                         characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
 
@@ -255,10 +308,56 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var currentRun = _textRuns[index];
 
-                    if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
-                        flowDirection, out var distance, out _))
+                    if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        var i = index;
+
+                        var rightToLeftWidth = currentRun.Size.Width;
+
+                        while (i + 1 <= _textRuns.Count - 1)
+                        {
+                            var nextRun = _textRuns[i + 1];
+
+                            if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
+                            {
+                                i++;
+
+                                rightToLeftWidth += nextRun.Size.Width;
+
+                                continue;
+                            }
+                            
+                            break;
+                        }
+
+                        if(i > index)
+                        {
+                            while (i >= index)
+                            {
+                                currentRun = _textRuns[i];
+
+                                rightToLeftWidth -= currentRun.Size.Width;
+
+                                if (currentPosition + currentRun.TextSourceLength >= characterIndex)
+                                {
+                                    break;
+                                }
+
+                                currentPosition += currentRun.TextSourceLength;
+
+                                remainingLength -= currentRun.TextSourceLength;
+
+                                i--;
+                            }
+
+                            currentDistance += rightToLeftWidth;
+                        }
+                    }
+
+                    if (currentPosition + currentRun.TextSourceLength >= characterIndex && 
+                        TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
                     {
-                        return currentDistance + distance;
+                        return Math.Max(0, currentDistance + distance);
                     }
 
                     //No hit hit found so we add the full width
@@ -283,7 +382,7 @@ namespace Avalonia.Media.TextFormatting
                             distance = currentGlyphRun.Size.Width - distance;
                         }
 
-                        return currentDistance - distance;
+                        return Math.Max(0, currentDistance - distance);
                     }
 
                     //No hit hit found so we add the full width
@@ -293,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return currentDistance;
+            return Math.Max(0, currentDistance);
         }
 
         private static bool TryGetDistanceFromCharacterHit(
@@ -442,92 +541,139 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                {
-                    startX += currentRun.Size.Width;
-
-                    currentPosition += currentRun.TextSourceLength;
-
-                    continue;
-                }
-
                 var characterLength = 0;
                 var endX = startX;
+                var runWidth = 0.0;
+                TextRunBounds? currentRunBounds = null;
 
-                if (currentRun is ShapedTextCharacters currentShapedRun)
+                var currentShapedRun = currentRun as ShapedTextCharacters;
+
+                if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
                 {
-                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+                    var rightToLeftIndex = index;
+                    startX += currentShapedRun.Size.Width;
 
-                    currentPosition += offset;
+                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    {
+                        var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
 
-                    var startIndex = currentRun.Text.Start + offset;
+                        if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+                        {
+                            break;
+                        }
 
-                    double startOffset;
-                    double endOffset;
+                        startX += nextShapedRun.Size.Width;
 
-                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                        rightToLeftIndex++;
+                    }
+
+                    if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
                     {
-                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        startX = currentRunBounds!.Rectangle.Left;
+                        endX = currentRunBounds.Rectangle.Right;
 
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                        runWidth = currentRunBounds.Rectangle.Width;
                     }
-                    else
+
+                    currentDirection = FlowDirection.RightToLeft;
+                }
+                else
+                {
+                    if (currentShapedRun != null)
                     {
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
 
-                        if (currentPosition < startIndex)
+                            currentPosition += currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+                        currentPosition += offset;
+
+                        var startIndex = currentRun.Text.Start + offset;
+
+                        double startOffset;
+                        double endOffset;
+
+                        if (currentShapedRun.ShapedBuffer.IsLeftToRight)
                         {
-                            startOffset = endOffset;
+                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
                         }
                         else
                         {
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                            if (currentPosition < startIndex)
+                            {
+                                startOffset = endOffset;
+                            }
+                            else
+                            {
+                                startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                            }
                         }
-                    }
 
-                    startX += startOffset;
+                        startX += startOffset;
 
-                    endX += endOffset;
+                        endX += endOffset;
 
-                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
 
-                    characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
 
-                    currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
-                        FlowDirection.LeftToRight :
-                        FlowDirection.RightToLeft;
-                }
-                else
-                {
-                    if (currentPosition < firstTextSourceIndex)
+                        currentDirection = FlowDirection.LeftToRight;
+                    }
+                    else
                     {
-                        startX += currentRun.Size.Width;
+                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
+
+                            currentPosition += currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        if (currentPosition < firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
+                        }
+
+                        if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                        {
+                            endX += currentRun.Size.Width;
+
+                            characterLength = currentRun.TextSourceLength;
+                        }
                     }
 
-                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    if (endX < startX)
                     {
-                        endX += currentRun.Size.Width;
+                        (endX, startX) = (startX, endX);
+                    }
 
-                        characterLength = currentRun.TextSourceLength;
+                    //Lines that only contain a linebreak need to be covered here
+                    if (characterLength == 0)
+                    {
+                        characterLength = NewLineLength;
                     }
-                }
 
-                if (endX < startX)
-                {
-                    (endX, startX) = (startX, endX);
-                }
+                    runWidth = endX - startX;
+                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
 
-                //Lines that only contain a linebreak need to be covered here
-                if (characterLength == 0)
-                {
-                    characterLength = NewLineLength;
-                }
+                    currentPosition += characterLength;
 
-                var runWidth = endX - startX;
-                var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+                    remainingLength -= characterLength;
+                }                 
 
-                if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+                if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
                 {
                     if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
                     {
@@ -537,32 +683,26 @@ namespace Avalonia.Media.TextFormatting
 
                         textBounds.Rectangle = currentRect;
 
-                        textBounds.TextRunBounds.Add(currentRunBounds);
+                        textBounds.TextRunBounds.Add(currentRunBounds!);
                     }
                     else
                     {
-                        currentRect = currentRunBounds.Rectangle;
+                        currentRect = currentRunBounds!.Rectangle;
 
                         result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
                     }
                 }
 
                 currentWidth += runWidth;
-                currentPosition += characterLength;
+              
 
-                if (currentPosition > characterIndex)
+                if (remainingLength <= 0 || currentPosition >= characterIndex)
                 {
                     break;
                 }
 
                 startX = endX;
                 lastDirection = currentDirection;
-                remainingLength -= characterLength;
-
-                if (remainingLength <= 0)
-                {
-                    break;
-                }
             }
 
             return result;
@@ -674,7 +814,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
 
-                if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+                if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
                 {
                     if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
                     {
@@ -692,7 +832,7 @@ namespace Avalonia.Media.TextFormatting
 
                         result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
                     }
-                }               
+                }
 
                 currentWidth += runWidth;
                 currentPosition += characterLength;
@@ -716,6 +856,107 @@ namespace Avalonia.Media.TextFormatting
             return result;
         }
 
+        private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
+        {
+            textRunBounds = null;
+
+            for (var index = runIndex; index >= 0; index--)
+            {
+                if (TextRuns[index] is not DrawableTextRun currentRun)
+                {
+                    continue;
+                }
+
+                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                {
+                    startX -= currentRun.Size.Width;
+
+                    currentPosition += currentRun.TextSourceLength;
+
+                    continue;
+                }
+
+                var characterLength = 0;
+                var endX = startX;
+
+                if (currentRun is ShapedTextCharacters currentShapedRun)
+                {
+                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+                    currentPosition += offset;
+
+                    var startIndex = currentRun.Text.Start + offset;
+                    double startOffset;
+                    double endOffset;
+
+                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        if (currentPosition < startIndex)
+                        {
+                            startOffset = endOffset = 0;
+                        }
+                        else
+                        {
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+
+                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        }
+                    }
+                    else
+                    {
+                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                    }
+
+                    startX -= currentRun.Size.Width - startOffset;
+                    endX -= currentRun.Size.Width - endOffset;
+
+                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+                    characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+                }
+                else
+                {
+                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    {
+                        endX -= currentRun.Size.Width;
+                    }
+
+                    if (currentPosition < firstTextSourceIndex)
+                    {
+                        startX -= currentRun.Size.Width;
+
+                        characterLength = currentRun.TextSourceLength;
+                    }
+                }
+
+                if (endX < startX)
+                {
+                    (endX, startX) = (startX, endX);
+                }
+
+                //Lines that only contain a linebreak need to be covered here
+                if (characterLength == 0)
+                {
+                    characterLength = NewLineLength;
+                }
+
+                var runWidth = endX - startX;
+
+                remainingLength -= characterLength;
+
+                currentPosition += characterLength;
+
+                textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+
+                return true;
+            }
+
+            return false;
+        }
+
         public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
         {
             if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
@@ -1295,6 +1536,11 @@ namespace Avalonia.Media.TextFormatting
             var textAlignment = _paragraphProperties.TextAlignment;
             var paragraphFlowDirection = _paragraphProperties.FlowDirection;
 
+            if(textAlignment == TextAlignment.Justify)
+            {
+                textAlignment = TextAlignment.Start;
+            }
+
             switch (textAlignment)
             {
                 case TextAlignment.Start:
@@ -1319,12 +1565,12 @@ namespace Avalonia.Media.TextFormatting
                 case TextAlignment.Center:
                     var start = (_paragraphWidth - width) / 2;
 
-                    if(paragraphFlowDirection == FlowDirection.RightToLeft)
+                    if (paragraphFlowDirection == FlowDirection.RightToLeft)
                     {
                         start -= (widthIncludingTrailingWhitespace - width);
                     }
 
-                    return Math.Max(0,  start);                         
+                    return Math.Max(0, start);
                 case TextAlignment.Right:
                     return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
 

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@@ -224,7 +224,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         }
 
         /// <summary>
-        /// Returns <see langword="true"/> if <paramref name="value"/> is between
+        /// Returns <see langword="true"/> if <paramref name="cp"/> is between
         /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
         /// </summary>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]

+ 4 - 4
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile
         return Task.FromResult<IStorageFolder?>(null);
     }
 
-    public Task<Stream> OpenRead()
+    public Task<Stream> OpenReadAsync()
     {
         return Task.FromResult<Stream>(_fileInfo.OpenRead());
     }
 
-    public Task<Stream> OpenWrite()
+    public Task<Stream> OpenWriteAsync()
     {
         return Task.FromResult<Stream>(_fileInfo.OpenWrite());
     }
 
-    public virtual Task<string?> SaveBookmark()
+    public virtual Task<string?> SaveBookmarkAsync()
     {
         return Task.FromResult<string?>(_fileInfo.FullName);
     }
 
-    public Task ReleaseBookmark()
+    public Task ReleaseBookmarkAsync()
     {
         // No-op
         return Task.CompletedTask;

+ 14 - 2
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Linq;
 using System.Security;
 using System.Threading.Tasks;
 using Avalonia.Metadata;
@@ -43,12 +45,22 @@ public class BclStorageFolder : IStorageBookmarkFolder
         return Task.FromResult<IStorageFolder?>(null);
     }
 
-    public virtual Task<string?> SaveBookmark()
+    public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    {
+         var items = _directoryInfo.GetDirectories()
+            .Select(d => (IStorageItem)new BclStorageFolder(d))
+            .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
+            .ToArray();
+
+         return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
+    }
+
+    public virtual Task<string?> SaveBookmarkAsync()
     {
         return Task.FromResult<string?>(_directoryInfo.FullName);
     }
     
-    public Task ReleaseBookmark()
+    public Task ReleaseBookmarkAsync()
     {
         // No-op
         return Task.CompletedTask;

+ 1 - 1
src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage;
 [NotClientImplementable]
 public interface IStorageBookmarkItem : IStorageItem
 {
-    Task ReleaseBookmark();
+    Task ReleaseBookmarkAsync();
 }
 
 [NotClientImplementable]

+ 2 - 2
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem
     /// <summary>
     /// Opens a stream for read access.
     /// </summary>
-    Task<Stream> OpenRead();
+    Task<Stream> OpenReadAsync();
 
     /// <summary>
     /// Returns true, if file is writeable. 
@@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem
     /// <summary>
     /// Opens stream for writing to the file.
     /// </summary>
-    Task<Stream> OpenWrite();
+    Task<Stream> OpenWriteAsync();
 }

+ 10 - 1
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@@ -1,4 +1,6 @@
-using Avalonia.Metadata;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
 
 namespace Avalonia.Platform.Storage;
 
@@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage;
 [NotClientImplementable]
 public interface IStorageFolder : IStorageItem
 {
+    /// <summary>
+    /// Gets the files and subfolders in the current folder.
+    /// </summary>
+    /// <returns>
+    /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an <see cref="IStorageItem"/> implementation object.
+    /// </returns>
+    Task<IReadOnlyList<IStorageItem>> GetItemsAsync();
 }

+ 1 - 1
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable
     /// <returns>
     /// Returns identifier of a bookmark. Can be null if OS denied request.
     /// </returns>
-    Task<string?> SaveBookmark();
+    Task<string?> SaveBookmarkAsync();
 
     /// <summary>
     /// Gets the parent folder of the current storage item.

+ 2 - 2
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs

@@ -13,10 +13,10 @@ namespace Avalonia.Rendering.Composition.Animations
     /// This is the base class for ExpressionAnimation and KeyFrameAnimation.
     /// </summary>
     /// <remarks>
-    /// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
+    /// Use the <see cref="CompositionObject.StartAnimation(string , CompositionAnimation)"/> method to start the animation.
     /// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
     /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
-    /// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
+    /// Changing the value of the variable after <see cref="CompositionObject.StartAnimation(string , CompositionAnimation)"/> is called will not affect
     /// the value of the ExpressionAnimation.
     /// See the remarks section of ExpressionAnimation for additional information.
     /// </remarks>

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations
     /// This contrasts <see cref="KeyFrameAnimation"/>s, which use an interpolator to define how the animating
     /// property changes over time. The mathematical equation can be defined using references to properties
     /// of Composition objects, mathematical functions and operators and Input.
-    /// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
+    /// Use the <see cref="CompositionObject.StartAnimation(string , CompositionAnimation)"/> method to start the animation.
     /// </remarks>
     public class ExpressionAnimation : CompositionAnimation
     {

+ 2 - 2
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs

@@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition.Animations
         /// The delay behavior of the key frame animation.
         /// </summary>
         public AnimationDelayBehavior DelayBehavior { get; set; }
-        
+
         /// <summary>
-        /// Delay before the animation starts after <see cref="CompositionObject.StartAnimation"/> is called.
+        /// Delay before the animation starts after <see cref="CompositionObject.StartAnimation(string , CompositionAnimation)"/> is called.
         /// </summary>
         public System.TimeSpan DelayTime { get; set; }
         

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
-        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public GeometryNode(Matrix transform,
             IBrush? brush,
             IPen? pen,

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="transform">The transform.</param>
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="glyphRun">The glyph run to draw.</param>
-        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public GlyphRunNode(
             Matrix transform,
             IBrush foreground,

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The start point of the line.</param>
         /// <param name="p2">The end point of the line.</param>
-        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public LineNode(
             Matrix transform,
             IPen pen,

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         /// <param name="mask">The opacity mask to push.</param>
         /// <param name="bounds">The bounds of the mask.</param>
-        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null)
             : base(Rect.Empty, Matrix.Identity, aux)
         {

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="pen">The stroke pen.</param>
         /// <param name="rect">The rectangle to draw.</param>
         /// <param name="boxShadows">The box shadow parameters</param>
-        /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
+        /// <param name="aux">Auxiliary data required to draw the brush.</param>
         public RectangleNode(
             Matrix transform,
             IBrush? brush,

+ 1 - 1
src/Avalonia.Base/Utilities/MathUtilities.cs

@@ -255,7 +255,7 @@ namespace Avalonia.Utilities
         /// <summary>
         /// Clamps a value between a minimum and maximum value.
         /// </summary>
-        /// <param name="val">The value.</param>
+        /// <param name="value">The value.</param>
         /// <param name="min">The minimum value.</param>
         /// <param name="max">The maximum value.</param>
         /// <returns>The clamped value.</returns>

+ 11 - 12
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

@@ -1,15 +1,14 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
-        x:CompileBindings="True">
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
+                    x:CompileBindings="True">
 
-  <Styles.Resources>
-    <pc:AccentColorConverter x:Key="AccentColorConverter" />
-    <x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double>
-    <x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
-  </Styles.Resources>
+  <pc:AccentColorConverter x:Key="AccentColorConverter" />
+  <x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double>
+  <x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
 
-  <Style Selector="ColorPreviewer">
+  <ControlTheme x:Key="{x:Type ColorPreviewer}"
+                TargetType="ColorPreviewer">
     <Setter Property="Height" Value="70" />
     <Setter Property="CornerRadius" Value="0" />
     <Setter Property="Template">
@@ -97,6 +96,6 @@
         </Panel>
       </ControlTemplate>
     </Setter>
-  </Style>
+  </ControlTheme>
 
-</Styles>
+</ResourceDictionary>

+ 174 - 172
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

@@ -1,188 +1,190 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        x:CompileBindings="True">
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    x:CompileBindings="True">
 
-  <Style Selector="Thumb.ColorSliderThumbStyle">
-    <Setter Property="BorderThickness" Value="0" />
+  <ControlTheme x:Key="ColorSliderThumbTheme"
+                TargetType="Thumb">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
+    <Setter Property="BorderThickness" Value="3" />
+    <Setter Property="CornerRadius" Value="10" />
     <Setter Property="Template">
       <Setter.Value>
         <ControlTemplate>
           <Border Background="{TemplateBinding Background}"
                   BorderBrush="{TemplateBinding BorderBrush}"
                   BorderThickness="{TemplateBinding BorderThickness}"
-                  CornerRadius="10" />
+                  CornerRadius="{TemplateBinding CornerRadius}" />
         </ControlTemplate>
       </Setter.Value>
     </Setter>
-  </Style>
+  </ControlTheme>
 
-  <Style Selector="ColorSlider:horizontal">
-    <Setter Property="BorderThickness" Value="0" />
-    <Setter Property="CornerRadius" Value="10" />
-    <Setter Property="Height" Value="20" />
-    <Setter Property="Template">
-      <ControlTemplate TargetType="{x:Type ColorSlider}">
-        <Border BorderThickness="{TemplateBinding BorderThickness}"
-                BorderBrush="{TemplateBinding BorderBrush}"
-                CornerRadius="{TemplateBinding CornerRadius}">
-          <Grid Margin="{TemplateBinding Padding}">
-            <Rectangle HorizontalAlignment="Stretch"
-                       VerticalAlignment="Stretch"
-                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
-            <Rectangle HorizontalAlignment="Stretch"
-                       VerticalAlignment="Stretch"
-                       Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
-            <Track Name="PART_Track"
-                   HorizontalAlignment="Stretch"
-                   VerticalAlignment="Stretch"
-                   Minimum="{TemplateBinding Minimum}"
-                   Maximum="{TemplateBinding Maximum}"
-                   Value="{TemplateBinding Value, Mode=TwoWay}"
-                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
-                   Orientation="Horizontal">
-              <Track.DecreaseButton>
-                <RepeatButton Name="PART_DecreaseButton"
-                              Background="Transparent"
-                              Focusable="False"
-                              HorizontalAlignment="Stretch"
-                              VerticalAlignment="Stretch">
-                  <RepeatButton.Template>
-                    <ControlTemplate>
-                      <Border Name="FocusTarget"
-                              Background="Transparent"
-                              Margin="0,-10" />
-                    </ControlTemplate>
-                  </RepeatButton.Template>
-                </RepeatButton>
-              </Track.DecreaseButton>
-              <Track.IncreaseButton>
-                <RepeatButton Name="PART_IncreaseButton"
-                              Background="Transparent"
-                              Focusable="False"
-                              HorizontalAlignment="Stretch"
-                              VerticalAlignment="Stretch">
-                  <RepeatButton.Template>
-                    <ControlTemplate>
-                      <Border Name="FocusTarget"
-                              Background="Transparent"
-                              Margin="0,-10" />
-                    </ControlTemplate>
-                  </RepeatButton.Template>
-                </RepeatButton>
-              </Track.IncreaseButton>
-              <Thumb Classes="ColorSliderThumbStyle"
-                     Name="ColorSliderThumb"
-                     Margin="0"
-                     Padding="0"
-                     DataContext="{TemplateBinding Value}"
-                     Height="{TemplateBinding Height}"
-                     Width="{TemplateBinding Height}" />
-            </Track>
-          </Grid>
-        </Border>
-      </ControlTemplate>
-    </Setter>
-  </Style>
+  <ControlTheme x:Key="{x:Type ColorSlider}"
+                TargetType="ColorSlider">
 
-  <Style Selector="ColorSlider:vertical">
-    <Setter Property="BorderThickness" Value="0" />
-    <Setter Property="CornerRadius" Value="10" />
-    <Setter Property="Width" Value="20" />
-    <Setter Property="Template">
-      <ControlTemplate TargetType="{x:Type ColorSlider}">
-        <Border BorderThickness="{TemplateBinding BorderThickness}"
-                BorderBrush="{TemplateBinding BorderBrush}"
-                CornerRadius="{TemplateBinding CornerRadius}">
-          <Grid Margin="{TemplateBinding Padding}">
-            <Rectangle HorizontalAlignment="Stretch"
-                       VerticalAlignment="Stretch"
-                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
-            <Rectangle HorizontalAlignment="Stretch"
-                       VerticalAlignment="Stretch"
-                       Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
-            <Track Name="PART_Track"
-                   HorizontalAlignment="Stretch"
-                   VerticalAlignment="Stretch"
-                   Minimum="{TemplateBinding Minimum}"
-                   Maximum="{TemplateBinding Maximum}"
-                   Value="{TemplateBinding Value, Mode=TwoWay}"
-                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
-                   Orientation="Vertical">
-              <Track.DecreaseButton>
-                <RepeatButton Name="PART_DecreaseButton"
-                              Background="Transparent"
-                              Focusable="False"
-                              HorizontalAlignment="Stretch"
-                              VerticalAlignment="Stretch">
-                  <RepeatButton.Template>
-                    <ControlTemplate>
-                      <Border Name="FocusTarget"
-                              Background="Transparent"
-                              Margin="0,-10" />
-                    </ControlTemplate>
-                  </RepeatButton.Template>
-                </RepeatButton>
-              </Track.DecreaseButton>
-              <Track.IncreaseButton>
-                <RepeatButton Name="PART_IncreaseButton"
-                              Background="Transparent"
-                              Focusable="False"
-                              HorizontalAlignment="Stretch"
-                              VerticalAlignment="Stretch">
-                  <RepeatButton.Template>
-                    <ControlTemplate>
-                      <Border Name="FocusTarget"
-                              Background="Transparent"
-                              Margin="0,-10" />
-                    </ControlTemplate>
-                  </RepeatButton.Template>
-                </RepeatButton>
-              </Track.IncreaseButton>
-              <Thumb Classes="ColorSliderThumbStyle"
-                     Name="ColorSliderThumb"
-                     Margin="0"
-                     Padding="0"
-                     DataContext="{TemplateBinding Value}"
-                     Height="{TemplateBinding Width}"
-                     Width="{TemplateBinding Width}" />
-            </Track>
-          </Grid>
-        </Border>
-      </ControlTemplate>
-    </Setter>
-  </Style>
+    <Style Selector="^:horizontal">
+      <Setter Property="BorderThickness" Value="0" />
+      <Setter Property="CornerRadius" Value="10" />
+      <Setter Property="Height" Value="20" />
+      <Setter Property="Template">
+        <ControlTemplate TargetType="{x:Type ColorSlider}">
+          <Border BorderThickness="{TemplateBinding BorderThickness}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  CornerRadius="{TemplateBinding CornerRadius}">
+            <Grid Margin="{TemplateBinding Padding}">
+              <Rectangle HorizontalAlignment="Stretch"
+                         VerticalAlignment="Stretch"
+                         Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
+              <Rectangle HorizontalAlignment="Stretch"
+                         VerticalAlignment="Stretch"
+                         Fill="{TemplateBinding Background}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
+              <Track Name="PART_Track"
+                     HorizontalAlignment="Stretch"
+                     VerticalAlignment="Stretch"
+                     Minimum="{TemplateBinding Minimum}"
+                     Maximum="{TemplateBinding Maximum}"
+                     Value="{TemplateBinding Value, Mode=TwoWay}"
+                     IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                     Orientation="Horizontal">
+                <Track.DecreaseButton>
+                  <RepeatButton Name="PART_DecreaseButton"
+                                Background="Transparent"
+                                Focusable="False"
+                                HorizontalAlignment="Stretch"
+                                VerticalAlignment="Stretch">
+                    <RepeatButton.Template>
+                      <ControlTemplate>
+                        <Border Name="FocusTarget"
+                                Background="Transparent"
+                                Margin="0,-10" />
+                      </ControlTemplate>
+                    </RepeatButton.Template>
+                  </RepeatButton>
+                </Track.DecreaseButton>
+                <Track.IncreaseButton>
+                  <RepeatButton Name="PART_IncreaseButton"
+                                Background="Transparent"
+                                Focusable="False"
+                                HorizontalAlignment="Stretch"
+                                VerticalAlignment="Stretch">
+                    <RepeatButton.Template>
+                      <ControlTemplate>
+                        <Border Name="FocusTarget"
+                                Background="Transparent"
+                                Margin="0,-10" />
+                      </ControlTemplate>
+                    </RepeatButton.Template>
+                  </RepeatButton>
+                </Track.IncreaseButton>
+                <Thumb Name="ColorSliderThumb"
+                       Theme="{StaticResource ColorSliderThumbTheme}"
+                       Margin="0"
+                       Padding="0"
+                       DataContext="{TemplateBinding Value}"
+                       Height="{TemplateBinding Height}"
+                       Width="{TemplateBinding Height}" />
+              </Track>
+            </Grid>
+          </Border>
+        </ControlTemplate>
+      </Setter>
+    </Style>
 
-  <!-- Normal State -->
-  <Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="Background" Value="Transparent" />
-    <Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
-    <Setter Property="BorderThickness" Value="3" />
-  </Style>
+    <Style Selector="^:vertical">
+      <Setter Property="BorderThickness" Value="0" />
+      <Setter Property="CornerRadius" Value="10" />
+      <Setter Property="Width" Value="20" />
+      <Setter Property="Template">
+        <ControlTemplate TargetType="{x:Type ColorSlider}">
+          <Border BorderThickness="{TemplateBinding BorderThickness}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  CornerRadius="{TemplateBinding CornerRadius}">
+            <Grid Margin="{TemplateBinding Padding}">
+              <Rectangle HorizontalAlignment="Stretch"
+                         VerticalAlignment="Stretch"
+                         Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
+              <Rectangle HorizontalAlignment="Stretch"
+                         VerticalAlignment="Stretch"
+                         Fill="{TemplateBinding Background}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
+              <Track Name="PART_Track"
+                     HorizontalAlignment="Stretch"
+                     VerticalAlignment="Stretch"
+                     Minimum="{TemplateBinding Minimum}"
+                     Maximum="{TemplateBinding Maximum}"
+                     Value="{TemplateBinding Value, Mode=TwoWay}"
+                     IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                     Orientation="Vertical">
+                <Track.DecreaseButton>
+                  <RepeatButton Name="PART_DecreaseButton"
+                                Background="Transparent"
+                                Focusable="False"
+                                HorizontalAlignment="Stretch"
+                                VerticalAlignment="Stretch">
+                    <RepeatButton.Template>
+                      <ControlTemplate>
+                        <Border Name="FocusTarget"
+                                Background="Transparent"
+                                Margin="0,-10" />
+                      </ControlTemplate>
+                    </RepeatButton.Template>
+                  </RepeatButton>
+                </Track.DecreaseButton>
+                <Track.IncreaseButton>
+                  <RepeatButton Name="PART_IncreaseButton"
+                                Background="Transparent"
+                                Focusable="False"
+                                HorizontalAlignment="Stretch"
+                                VerticalAlignment="Stretch">
+                    <RepeatButton.Template>
+                      <ControlTemplate>
+                        <Border Name="FocusTarget"
+                                Background="Transparent"
+                                Margin="0,-10" />
+                      </ControlTemplate>
+                    </RepeatButton.Template>
+                  </RepeatButton>
+                </Track.IncreaseButton>
+                <Thumb Name="ColorSliderThumb"
+                       Theme="{StaticResource ColorSliderThumbTheme}"
+                       Margin="0"
+                       Padding="0"
+                       DataContext="{TemplateBinding Value}"
+                       Height="{TemplateBinding Width}"
+                       Width="{TemplateBinding Width}" />
+              </Track>
+            </Grid>
+          </Border>
+        </ControlTemplate>
+      </Setter>
+    </Style>
+
+    <!-- Selector/Thumb Color -->
+    <Style Selector="^:pointerover /template/ Thumb#ColorSliderThumb">
+      <Setter Property="Opacity" Value="0.75" />
+    </Style>
+    <Style Selector="^:pointerover:dark-selector /template/ Thumb#ColorSliderThumb">
+      <Setter Property="Opacity" Value="0.7" />
+    </Style>
+    <Style Selector="^:pointerover:light-selector /template/ Thumb#ColorSliderThumb">
+      <Setter Property="Opacity" Value="0.8" />
+    </Style>
 
-  <!-- Selector/Thumb Color -->
-  <Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="Opacity" Value="0.75" />
-  </Style>
-  <Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="Opacity" Value="0.7" />
-  </Style>
-  <Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="Opacity" Value="0.8" />
-  </Style>
+    <Style Selector="^:dark-selector /template/ Thumb#ColorSliderThumb">
+      <Setter Property="BorderBrush" Value="Black" />
+    </Style>
+    <Style Selector="^:light-selector /template/ Thumb#ColorSliderThumb">
+      <Setter Property="BorderBrush" Value="White" />
+    </Style>
 
-  <Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="BorderBrush" Value="Black" />
-  </Style>
-  <Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
-    <Setter Property="BorderBrush" Value="White" />
-  </Style>
+  </ControlTheme>
 
-</Styles>
+</ResourceDictionary>

+ 48 - 46
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@@ -1,9 +1,10 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:controls="using:Avalonia.Controls"
-        x:CompileBindings="True">
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:controls="using:Avalonia.Controls"
+                    x:CompileBindings="True">
 
-  <Style Selector="ColorSpectrum">
+  <ControlTheme x:Key="{x:Type ColorSpectrum}"
+                TargetType="ColorSpectrum">
     <Setter Property="Template">
       <Setter.Value>
         <ControlTemplate TargetType="{x:Type ColorSpectrum}">
@@ -79,50 +80,51 @@
         </ControlTemplate>
       </Setter.Value>
     </Setter>
-  </Style>
 
-  <!-- Normal -->
-  <!-- Separating this allows easier customization in applications -->
-  <Style Selector="ColorSpectrum /template/ Ellipse#BorderEllipse,
-                   ColorSpectrum /template/ Rectangle#BorderRectangle">
-    <Setter Property="Stroke" Value="{DynamicResource ThemeBorderLowBrush}" />
-    <Setter Property="StrokeThickness" Value="1" />
-  </Style>
+    <!-- Normal -->
+    <!-- Separating this allows easier customization in applications -->
+    <Style Selector="^ /template/ Ellipse#BorderEllipse,
+                     ^ /template/ Rectangle#BorderRectangle">
+      <Setter Property="Stroke" Value="{DynamicResource ThemeBorderLowBrush}" />
+      <Setter Property="StrokeThickness" Value="1" />
+    </Style>
 
-  <!-- Focus -->
-  <Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
-    <Setter Property="IsVisible" Value="False" />
-  </Style>
-  <Style Selector="ColorSpectrum:focus-visible /template/ Ellipse#FocusEllipse">
-    <Setter Property="IsVisible" Value="True" />
-  </Style>
+    <!-- Focus -->
+    <Style Selector="^ /template/ Ellipse#FocusEllipse">
+      <Setter Property="IsVisible" Value="False" />
+    </Style>
+    <Style Selector="^:focus-visible /template/ Ellipse#FocusEllipse">
+      <Setter Property="IsVisible" Value="True" />
+    </Style>
 
-  <!-- Selector Color -->
-  <Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
-    <Setter Property="Stroke" Value="White" />
-  </Style>
-  <Style Selector="ColorSpectrum /template/ Ellipse#SelectionEllipse">
-    <Setter Property="Stroke" Value="Black" />
-  </Style>
-  <Style Selector="ColorSpectrum:light-selector /template/ Ellipse#FocusEllipse">
-    <Setter Property="Stroke" Value="Black" />
-  </Style>
-  <Style Selector="ColorSpectrum:light-selector /template/ Ellipse#SelectionEllipse">
-    <Setter Property="Stroke" Value="White" />
-  </Style>
+    <!-- Selector Color -->
+    <Style Selector="^ /template/ Ellipse#FocusEllipse">
+      <Setter Property="Stroke" Value="White" />
+    </Style>
+    <Style Selector="^ /template/ Ellipse#SelectionEllipse">
+      <Setter Property="Stroke" Value="Black" />
+    </Style>
+    <Style Selector="^:light-selector /template/ Ellipse#FocusEllipse">
+      <Setter Property="Stroke" Value="Black" />
+    </Style>
+    <Style Selector="^:light-selector /template/ Ellipse#SelectionEllipse">
+      <Setter Property="Stroke" Value="White" />
+    </Style>
 
-  <Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
-    <Setter Property="Opacity" Value="0.8" />
-  </Style>
+    <Style Selector="^:pointerover /template/ Ellipse#SelectionEllipse">
+      <Setter Property="Opacity" Value="0.8" />
+    </Style>
 
-  <!-- Selector Size -->
-  <Style Selector="ColorSpectrum /template/ Panel#PART_SelectionEllipsePanel">
-    <Setter Property="Width" Value="16" />
-    <Setter Property="Height" Value="16" />
-  </Style>
-  <Style Selector="ColorSpectrum:large-selector /template/ Panel#PART_SelectionEllipsePanel">
-    <Setter Property="Width" Value="48" />
-    <Setter Property="Height" Value="48" />
-  </Style>
+    <!-- Selector Size -->
+    <Style Selector="^ /template/ Panel#PART_SelectionEllipsePanel">
+      <Setter Property="Width" Value="16" />
+      <Setter Property="Height" Value="16" />
+    </Style>
+    <Style Selector="^:large-selector /template/ Panel#PART_SelectionEllipsePanel">
+      <Setter Property="Width" Value="48" />
+      <Setter Property="Height" Value="48" />
+    </Style>
 
-</Styles>
+  </ControlTheme>
+
+</ResourceDictionary>

+ 41 - 35
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

@@ -3,42 +3,48 @@
         xmlns:converters="using:Avalonia.Controls.Converters">
 
   <Styles.Resources>
-    <!-- Shared Resources -->
-    <VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
-                 TileMode="Tile"
-                 Stretch="Uniform"
-                 DestinationRect="0,0,8,8">
-      <VisualBrush.Visual>
-        <DrawingPresenter Width="8"
-                          Height="8">
-          <DrawingGroup>
-            <GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
-                             Brush="Transparent" />
-            <GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
-                             Brush="#19808080" />
-          </DrawingGroup>
-        </DrawingPresenter>
-      </VisualBrush.Visual>
-    </VisualBrush>
-    
-    <!-- Shared Converters -->
-    <converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
-    <converters:ToBrushConverter x:Key="ToBrushConverter" />
-    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
-    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
-    <converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/>
-    <converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/>
-    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" />
-    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" />
-  </Styles.Resources>
+    <ResourceDictionary>
+
+      <!-- Shared Resources -->
+      <VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
+                   TileMode="Tile"
+                   Stretch="Uniform"
+                   DestinationRect="0,0,8,8">
+        <VisualBrush.Visual>
+          <DrawingPresenter Width="8"
+                            Height="8">
+            <DrawingGroup>
+              <GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
+                               Brush="Transparent" />
+              <GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
+                               Brush="#19808080" />
+            </DrawingGroup>
+          </DrawingPresenter>
+        </VisualBrush.Visual>
+      </VisualBrush>
+
+      <!-- Shared Converters -->
+      <converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
+      <converters:ToBrushConverter x:Key="ToBrushConverter" />
+      <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+      <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
+      <converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/>
+      <converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/>
+      <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" />
+      <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" />
 
-  <!-- Primitives -->
-  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
-  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
-  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
+      <ResourceDictionary.MergedDictionaries>
 
-  <!-- Controls -->
-  <!-- Note the ColorPicker and ColorView are unsupported in the default theme -->
-  <!-- These controls depend on fluent styles for TabControl, Button, TextBox, etc. -->
+        <!-- Primitives -->
+        <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
+        <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
+        <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
 
+        <!-- Controls -->
+        <!-- Note the ColorPicker and ColorView are unsupported in the default theme -->
+        <!-- These controls depend on fluent styles for TabControl, Button, TextBox, etc. -->
+
+      </ResourceDictionary.MergedDictionaries>
+    </ResourceDictionary>
+  </Styles.Resources>
 </Styles>

+ 8 - 12
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -3,9 +3,6 @@
                     xmlns:controls="using:Avalonia.Controls"
                     x:CompileBindings="True">
 
-  <!-- This must follow OverlayCornerRadius -->
-  <CornerRadius x:Key="TopOverlayCornerRadius">5,5,0,0</CornerRadius>
-
   <ControlTheme x:Key="{x:Type ColorPicker}"
                 TargetType="ColorPicker">
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
@@ -25,7 +22,7 @@
                         Padding="0,0,10,0"
                         UseLayoutRounding="False">
           <DropDownButton.Styles>
-            <Style Selector="FlyoutPresenter.NoPadding">
+            <Style Selector="FlyoutPresenter.nopadding">
               <Setter Property="Padding" Value="0" />
             </Style>
           </DropDownButton.Styles>
@@ -45,7 +42,7 @@
             </Panel>
           </DropDownButton.Content>
           <DropDownButton.Flyout>
-            <Flyout FlyoutPresenterClasses="NoPadding">
+            <Flyout FlyoutPresenterClasses="nopadding">
               <ColorView x:Name="FlyoutColorView"
                          Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                          ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
@@ -73,7 +70,12 @@
                          PaletteColors="{TemplateBinding PaletteColors}"
                          PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
                          Palette="{TemplateBinding Palette}"
-                         SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+                         SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
+                <ColorView.Resources>
+                  <!-- This radius must follow OverlayCornerRadius -->
+                  <CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">5,5,0,0</CornerRadius>
+                </ColorView.Resources>
+              </ColorView>
             </Flyout>
           </DropDownButton.Flyout>
         </DropDownButton>
@@ -81,10 +83,4 @@
     </Setter>
   </ControlTheme>
 
-  <!-- Adjust Background within Flyout -->
-  <!-- Note: This is implemented but there seems to be an issue and the selector can't match across the Flyout -->
-  <!--<Style Selector="ColorPicker /template/ ColorView#FlyoutColorView /template/ Border#TabBackgroundBorder">
-    <Setter Property="CornerRadius" Value="{DynamicResource TopOverlayCornerRadius}" />
-  </Style>-->
-
 </ResourceDictionary>

+ 3 - 1
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -77,6 +77,8 @@
     17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5
     13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z
   </PathGeometry>
+  <!-- This radius should follow ControlCornerRadius -->
+  <CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">3</CornerRadius>
 
   <ControlTheme x:Key="{x:Type ColorView}"
                 TargetType="ColorView">
@@ -97,7 +99,7 @@
                   HorizontalAlignment="Stretch"
                   VerticalAlignment="Top"
                   Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
-                  CornerRadius="{TemplateBinding CornerRadius}" />
+                  CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
           <Border x:Name="ContentBackgroundBorder"
                   Grid.Row="0"
                   Grid.RowSpan="2"

+ 38 - 18
src/Avalonia.Controls/Button.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using System.Linq;
 using System.Windows.Input;
 using Avalonia.Automation.Peers;
@@ -281,24 +282,29 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {
-            if (e.Key == Key.Enter)
+            switch (e.Key)
             {
-                OnClick();
-                e.Handled = true;
-            }
-            else if (e.Key == Key.Space)
-            {
-                if (ClickMode == ClickMode.Press)
-                {
+                case Key.Enter:
                     OnClick();
+                    e.Handled = true;
+                    break;
+
+                case Key.Space:
+                {
+                    if (ClickMode == ClickMode.Press)
+                    {
+                        OnClick();
+                    }
+
+                    IsPressed = true;
+                    e.Handled = true;
+                    break;
                 }
-                IsPressed = true;
-                e.Handled = true;
-            }
-            else if (e.Key == Key.Escape && Flyout != null)
-            {
-                // If Flyout doesn't have focusable content, close the flyout here
-                Flyout.Hide();
+
+                case Key.Escape when Flyout != null:
+                    // If Flyout doesn't have focusable content, close the flyout here
+                    CloseFlyout();
+                    break;
             }
 
             base.OnKeyDown(e);
@@ -327,7 +333,14 @@ namespace Avalonia.Controls
         {
             if (IsEffectivelyEnabled)
             {
-                OpenFlyout();
+                if (_isFlyoutOpen)
+                {
+                    CloseFlyout();
+                }
+                else
+                {
+                    OpenFlyout();
+                }
 
                 var e = new RoutedEventArgs(ClickEvent);
                 RaiseEvent(e);
@@ -348,6 +361,14 @@ namespace Avalonia.Controls
             Flyout?.ShowAt(this);
         }
 
+        /// <summary>
+        /// Closes the button's flyout.
+        /// </summary>
+        protected virtual void CloseFlyout()
+        {
+            Flyout?.Hide();
+        }
+
         /// <summary>
         /// Invoked when the button's flyout is opened.
         /// </summary>
@@ -494,8 +515,7 @@ namespace Avalonia.Controls
 
                 // If flyout is changed while one is already open, make sure we 
                 // close the old one first
-                if (oldFlyout != null &&
-                    oldFlyout.IsOpen)
+                if (oldFlyout != null && oldFlyout.IsOpen)
                 {
                     oldFlyout.Hide();
                 }

+ 41 - 16
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -12,17 +12,12 @@ namespace Avalonia.Controls.Primitives
 {
     public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
     {
-        static FlyoutBase()
-        {
-            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
-        }
-
         /// <summary>
         /// Defines the <see cref="IsOpen"/> property
         /// </summary>
         public static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty =
-           AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
-               x => x.IsOpen);
+            AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
+                x => x.IsOpen);
 
         /// <summary>
         /// Defines the <see cref="Target"/> property
@@ -43,6 +38,14 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
                 x => x.ShowMode, (x, v) => x.ShowMode = v);
 
+        /// <summary>
+        /// Defines the <see cref="OverlayInputPassThroughElement"/> property
+        /// </summary>
+        public static readonly DirectProperty<FlyoutBase, IInputElement?> OverlayInputPassThroughElementProperty =
+            Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>(
+                o => o._overlayInputPassThroughElement,
+                (o, v) => o._overlayInputPassThroughElement = v);
+
         /// <summary>
         /// Defines the AttachedFlyout property
         /// </summary>
@@ -57,6 +60,12 @@ namespace Avalonia.Controls.Primitives
         private PixelRect? _enlargePopupRectScreenPixelRect;
         private IDisposable? _transientDisposable;
         private Action<IPopupHost?>? _popupHostChangedHandler;
+        private IInputElement? _overlayInputPassThroughElement;
+
+        static FlyoutBase()
+        {
+            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
+        }
 
         public FlyoutBase()
         {
@@ -101,11 +110,21 @@ namespace Avalonia.Controls.Primitives
             private set => SetAndRaise(TargetProperty, ref _target, value);
         }
 
+        /// <summary>
+        /// Gets or sets an element that should receive pointer input events even when underneath
+        /// the flyout's overlay.
+        /// </summary>
+        public IInputElement? OverlayInputPassThroughElement
+        {
+            get => _overlayInputPassThroughElement;
+            set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
+        }
+
         IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
 
-        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged 
-        { 
-            add => _popupHostChangedHandler += value; 
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
+        {
+            add => _popupHostChangedHandler += value;
             remove => _popupHostChangedHandler -= value;
         }
 
@@ -175,8 +194,9 @@ namespace Avalonia.Controls.Primitives
 
             IsOpen = false;
             Popup.IsOpen = false;
+
             ((ISetLogicalParent)Popup).SetParent(null);
-            
+
             // Ensure this isn't active
             _transientDisposable?.Dispose();
             _transientDisposable = null;
@@ -231,6 +251,8 @@ namespace Avalonia.Controls.Primitives
                 Popup.Child = CreatePresenter();
             }
 
+            Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
+
             if (CancelOpening())
             {
                 return false;
@@ -356,10 +378,13 @@ namespace Avalonia.Controls.Primitives
 
         private Popup CreatePopup()
         {
-            var popup = new Popup();
-            popup.WindowManagerAddShadowHint = false;
-            popup.IsLightDismissEnabled = true;
-            popup.OverlayDismissEventPassThrough = true;
+            var popup = new Popup
+            {
+                WindowManagerAddShadowHint = false,
+                IsLightDismissEnabled = true,
+                //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
+                OverlayDismissEventPassThrough = false
+            };
 
             popup.Opened += OnPopupOpened;
             popup.Closed += OnPopupClosed;
@@ -372,7 +397,7 @@ namespace Avalonia.Controls.Primitives
         {
             IsOpen = true;
 
-            _popupHostChangedHandler?.Invoke(Popup!.Host);
+            _popupHostChangedHandler?.Invoke(Popup.Host);
         }
 
         private void OnPopupClosing(object? sender, CancelEventArgs e)

+ 4 - 3
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
 using Avalonia.Layout;
 using Avalonia.Media.Immutable;
 using Avalonia.Controls.Documents;
+using Avalonia.Media.TextFormatting.Unicode;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -496,14 +497,14 @@ namespace Avalonia.Controls.Presenters
             var length = Math.Max(selectionStart, selectionEnd) - start;
 
             IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
-            
-            if (length > 0)
+
+            if (length > 0 && SelectionForegroundBrush != null)
             {
                 textStyleOverrides = new[]
                 {
                     new ValueSpan<TextRunProperties>(start, length,
                         new GenericTextRunProperties(typeface, FontSize,
-                            foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
+                            foregroundBrush: SelectionForegroundBrush))
                 };
             }
 

+ 1 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -501,7 +501,7 @@ namespace Avalonia.Controls.Primitives
                 if (dismissLayer != null)
                 {
                     dismissLayer.IsVisible = true;
-                    dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
+                    dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement;
                     
                     Disposable.Create(() =>
                     {

+ 12 - 12
src/Avalonia.Controls/RichTextBlock.cs

@@ -44,8 +44,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Inlines"/> property.
         /// </summary>
-        public static readonly StyledProperty<InlineCollection> InlinesProperty =
-            AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
+        public static readonly StyledProperty<InlineCollection?> InlinesProperty =
+            AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
                 nameof(Inlines));
 
         public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
@@ -138,7 +138,7 @@ namespace Avalonia.Controls
         /// Gets or sets the inlines.
         /// </summary>
         [Content]
-        public InlineCollection Inlines
+        public InlineCollection? Inlines
         {
             get => GetValue(InlinesProperty);
             set => SetValue(InlinesProperty, value);
@@ -159,7 +159,7 @@ namespace Avalonia.Controls
             remove => RemoveHandler(CopyingToClipboardEvent, value);
         }
 
-        internal bool HasComplexContent => Inlines.Count > 0;
+        internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
 
         /// <summary>
         /// Copies the current selection to the Clipboard.
@@ -260,23 +260,23 @@ namespace Avalonia.Controls
             {
                 if (!string.IsNullOrEmpty(_text))
                 {
-                    Inlines.Add(_text);
+                    Inlines?.Add(_text);
 
                     _text = null;
                 }
 
-                Inlines.Add(text);
+                Inlines?.Add(text);
             }
         }
 
         protected override string? GetText()
         {
-            return _text ?? Inlines.Text;
+            return _text ?? Inlines?.Text;
         }
 
         protected override void SetText(string? text)
         {
-            var oldValue = _text ?? Inlines?.Text;
+            var oldValue = GetText();
       
             AddText(text);        
 
@@ -301,10 +301,10 @@ namespace Avalonia.Controls
 
             ITextSource textSource;
 
-            var inlines = Inlines;
-
             if (HasComplexContent)
             {
+                var inlines = Inlines!;
+
                 var textRuns = new List<TextRun>();
 
                 foreach (var inline in inlines)
@@ -537,7 +537,7 @@ namespace Avalonia.Controls
 
             switch (change.Property.Name)
             {
-                case nameof(InlinesProperty):
+                case nameof(Inlines):
                     {
                         OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
                         InvalidateTextLayout();
@@ -553,7 +553,7 @@ namespace Avalonia.Controls
                 return "";
             }
 
-            var text = Inlines.Text ?? Text;
+            var text = GetText();
 
             if (string.IsNullOrEmpty(text))
             {

+ 4 - 2
src/Avalonia.Controls/TextBox.cs

@@ -17,6 +17,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Automation.Peers;
+using System.Diagnostics;
 
 namespace Avalonia.Controls
 {
@@ -1240,9 +1241,10 @@ namespace Avalonia.Controls
                     MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
                     MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
 
-                _presenter.MoveCaretToPoint(point);
+                _presenter.MoveCaretToPoint(point);  
 
                 var caretIndex = _presenter.CaretIndex;
+
                 var text = Text;
 
                 if (text != null && _wordSelectionStart >= 0)
@@ -1266,7 +1268,7 @@ namespace Avalonia.Controls
                 }
                 else
                 {
-                    SelectionEnd = _presenter.CaretIndex;
+                    SelectionEnd = caretIndex;
                 }
             }
         }

+ 29 - 4
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@@ -145,7 +145,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
 
         public bool CanBookmark => true;
 
-        public Task<string?> SaveBookmark()
+        public Task<string?> SaveBookmarkAsync()
         {
             return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask();
         }
@@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
             return Task.FromResult<IStorageFolder?>(null);
         }
 
-        public Task ReleaseBookmark()
+        public Task ReleaseBookmarkAsync()
         {
             return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask();
         }
@@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
         }
 
         public bool CanOpenRead => true;
-        public async Task<Stream> OpenRead()
+        public async Task<Stream> OpenReadAsync()
         {
             var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead");
             // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything.
@@ -182,7 +182,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
         }
 
         public bool CanOpenWrite => true;
-        public async Task<Stream> OpenWrite()
+        public async Task<Stream> OpenWriteAsync()
         {
             var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties");
             var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite");
@@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage
         public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle)
         {
         }
+
+        public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+        {
+            var items = await FileHandle.InvokeAsync<IJSInProcessObjectReference?>("getItems");
+            if (items is null)
+            {
+                return Array.Empty<IStorageItem>();
+            }
+
+            var count = items.Invoke<int>("count");
+
+            return Enumerable.Range(0, count)
+                .Select(index =>
+                {
+                    var reference = items.Invoke<IJSInProcessObjectReference>("at", index);
+                    return reference.Invoke<string>("getKind") switch
+                    {
+                        "directory" => (IStorageItem)new JSStorageFolder(reference),
+                        "file" => new JSStorageFile(reference),
+                        _ => null
+                    };
+                })
+                .Where(i => i is not null)
+                .ToArray()!;
+        }
     }
 }

+ 31 - 14
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts

@@ -14,6 +14,8 @@ declare global {
 
         queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
         requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
+
+        entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
     }
     type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; 
     type StartInDirectory =  WellKnownDirectory | FileSystemFileHandle;
@@ -53,7 +55,7 @@ class IndexedDbWrapper {
     }
 
     public connect(): Promise<InnerDbConnection> {
-        var conn = window.indexedDB.open(this.databaseName, 1);
+        const conn = window.indexedDB.open(this.databaseName, 1);
 
         conn.onupgradeneeded = event => {
             const db = (<IDBRequest<IDBDatabase>>event.target).result;
@@ -85,7 +87,7 @@ class InnerDbConnection {
         const os = this.openStore(store, "readwrite");
 
         return new Promise((resolve, reject) => {
-            var response = os.put(obj, key);
+            const response = os.put(obj, key);
             response.onsuccess = () => {
                 resolve(response.result);
             };
@@ -99,7 +101,7 @@ class InnerDbConnection {
         const os = this.openStore(store, "readonly");
 
         return new Promise((resolve, reject) => {
-            var response = os.get(key);
+            const response = os.get(key);
             response.onsuccess = () => {
                 resolve(response.result);
             };
@@ -113,7 +115,7 @@ class InnerDbConnection {
         const os = this.openStore(store, "readwrite");
 
         return new Promise((resolve, reject) => {
-            var response = os.delete(key);
+            const response = os.delete(key);
             response.onsuccess = () => {
                 resolve();
             };
@@ -134,17 +136,20 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
 ])
 
 class StorageItem {
-    constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { }
+    constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
 
     public getName(): string {
         return this.handle.name
     }
 
+    public getKind(): string {
+        return this.handle.kind;
+    }
+
     public async openRead(): Promise<Blob> {
         await this.verityPermissions('read');
 
-        var file = await this.handle.getFile();
-        return file;
+        return await this.handle.getFile();
     }
 
     public async openWrite(): Promise<FileSystemWritableFileStream> {
@@ -154,7 +159,7 @@ class StorageItem {
     }
 
     public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
-        var file = this.handle.getFile && await this.handle.getFile();
+        const file = this.handle.getFile && await this.handle.getFile();
         
         return file && {
             Size: file.size,
@@ -163,6 +168,18 @@ class StorageItem {
         }
     }
 
+    public async getItems(): Promise<StorageItems> {
+        if (this.handle.kind !== "directory"){
+            return new StorageItems([]);
+        }
+        
+        const items: StorageItem[] = [];
+        for await (const [key, value] of this.handle.entries()) {
+            items.push(new StorageItem(value));
+        }
+        return new StorageItems(items);
+    }
+    
     private async verityPermissions(mode: PermissionsMode): Promise<void | never> {
         if (await this.handle.queryPermission({ mode }) === 'granted') {
             return;
@@ -235,12 +252,12 @@ export class StorageProvider {
     }
 
     public static async selectFolderDialog(
-        startIn: StartInDirectory | null)
+        startIn: StorageItem | null)
         : Promise<StorageItem> {
 
         // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
         const options: DirectoryPickerOptions = {
-            startIn: (startIn || undefined)
+            startIn: (startIn?.handle || undefined)
         };
 
         const handle = await window.showDirectoryPicker(options);
@@ -248,12 +265,12 @@ export class StorageProvider {
     }
 
     public static async openFileDialog(
-        startIn: StartInDirectory | null, multiple: boolean,
+        startIn: StorageItem | null, multiple: boolean,
         types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
         : Promise<StorageItems> {
 
         const options: OpenFilePickerOptions = {
-            startIn: (startIn || undefined),
+            startIn: (startIn?.handle || undefined),
             multiple,
             excludeAcceptAllOption,
             types: (types || undefined)
@@ -264,12 +281,12 @@ export class StorageProvider {
     }
 
     public static async saveFileDialog(
-        startIn: StartInDirectory | null, suggestedName: string | null,
+        startIn: StorageItem | null, suggestedName: string | null,
         types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
         : Promise<StorageItem> {
 
         const options: SaveFilePickerOptions = {
-            startIn: (startIn || undefined),
+            startIn: (startIn?.handle || undefined),
             suggestedName: (suggestedName || undefined),
             excludeAcceptAllOption,
             types: (types || undefined)

+ 21 - 4
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Linq;
 using System.Threading.Tasks;
 using Avalonia.Logging;
 using Avalonia.Platform.Storage;
@@ -49,13 +51,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
         return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent()));
     }
 
-    public Task ReleaseBookmark()
+    public Task ReleaseBookmarkAsync()
     {
         // no-op
         return Task.CompletedTask;
     }
 
-    public Task<string?> SaveBookmark()
+    public Task<string?> SaveBookmarkAsync()
     {
         try
         {
@@ -102,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile
 
     public bool CanOpenWrite => true;
 
-    public Task<Stream> OpenRead()
+    public Task<Stream> OpenReadAsync()
     {
         return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read));
     }
 
-    public Task<Stream> OpenWrite()
+    public Task<Stream> OpenWriteAsync()
     {
         return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write));
     }
@@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
     public IOSStorageFolder(NSUrl url) : base(url)
     {
     }
+
+    public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    {
+        var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error);
+        if (error is not null)
+        {
+            return Task.FromException<IReadOnlyList<IStorageItem>>(new NSErrorException(error));
+        }
+
+        var items = content
+            .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u))
+            .ToArray();
+
+        return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
+    }
 }

+ 44 - 0
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@@ -48,5 +48,49 @@ namespace Avalonia.Controls.UnitTests
                 Assert.False(target.IsMeasureValid);
             }
         }
+
+        [Fact]
+        public void Changing_Inlines_Should_Invalidate_Measure()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new RichTextBlock();
+
+                var inlines = new InlineCollection { new Run("Hello") };
+
+                target.Measure(Size.Infinity);
+
+                Assert.True(target.IsMeasureValid);
+
+                target.Inlines = inlines;
+
+                Assert.False(target.IsMeasureValid);
+            }
+        }
+
+        [Fact]
+        public void Changing_Inlines_Should_Reset_Inlines_Parent()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new RichTextBlock();
+
+                var run = new Run("Hello");
+
+                target.Inlines.Add(run);
+
+                target.Measure(Size.Infinity);
+
+                Assert.True(target.IsMeasureValid);
+
+                target.Inlines = null;
+
+                Assert.Null(run.Parent);
+
+                target.Inlines = new InlineCollection { run };
+
+                Assert.Equal(target, run.Parent);
+            }
+        }
     }
 }

二進制
tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf


二進制
tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf


+ 7 - 1
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Skia.UnitTests.Media
         private readonly Typeface _defaultTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
         private readonly Typeface _arabicTypeface =
-           new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Kufi Arabic");
+           new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic");
         private readonly Typeface _italicTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic);
         private readonly Typeface _emojiTypeface =
@@ -82,6 +82,12 @@ namespace Avalonia.Skia.UnitTests.Media
                         skTypeface = typefaceCollection.Get(typeface);
                         break;
                     }
+                case "Noto Sans Arabic":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 case FontFamily.DefaultFontFamilyName:
                 case "Noto Mono":
                     {

+ 65 - 5
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Avalonia.Media;
@@ -914,14 +915,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         public void Should_Get_CharacterHit_From_Distance_RTL()
         {
             using (Start())
-            { 
+            {
                 var text = "أَبْجَدِيَّة عَرَبِيَّة";
 
                 var layout = new TextLayout(
-                  text,
-                  Typeface.Default,
-                  12,
-                  Brushes.Black);
+                    text,
+                    Typeface.Default,
+                    12,
+                    Brushes.Black);
 
                 var textLine = layout.TextLines[0];
 
@@ -952,6 +953,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 rect = layout.HitTestTextPosition(23);
 
                 Assert.Equal(0, rect.Left, 5);
+
+            }
+        }
+
+        [Fact]
+        public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles()
+        {
+            using (Start())
+            {
+                var text = "أَبْجَدِيَّة عَرَبِيَّة";
+
+                var i = 0;
+
+                var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());
+
+                while (graphemeEnumerator.MoveNext())
+                {
+                    var grapheme = graphemeEnumerator.Current;
+
+                    var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
+
+                    i += grapheme.Text.Length;
+
+                    var layout = new TextLayout(
+                        text,
+                        Typeface.Default,
+                        12,
+                        Brushes.Black,
+                        textStyleOverrides: textStyleOverrides);
+
+                    var textLine = layout.TextLines[0];
+
+                    var shapedRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
+
+                    var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList();
+
+                    var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList();
+
+                    var currentX = 0.0;
+
+                    var cluster = text.Length;
+
+                    for (int j = 0; j < clusters.Count - 1; j++)
+                    {                     
+                        var glyphAdvance = glyphAdvances[j];
+
+                        var characterHit = textLine.GetCharacterHitFromDistance(currentX);
+
+                        Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+                        var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
+
+                        Assert.Equal(currentX, distance, 5);
+
+                        currentX += glyphAdvance;
+
+                        cluster = clusters[j];
+                    }
+                }
             }
         }