diff --git a/Avalonia.sln b/Avalonia.sln index 4999719676..35b6b2108a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Simple", "src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edf3c3d819..52fc8db53c 100644 --- a/azure-pipelines.yml +++ b/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 diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 3e312c8685..175e838616 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -4,6 +4,6 @@ x:Class="BindingDemo.App"> - + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 7ebb87094a..750c1082a6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; @@ -23,9 +23,9 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml") }; - public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude ColorPickerSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml") }; public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) @@ -33,16 +33,16 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") }; - public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml") }; public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); - public static SimpleTheme Default = new SimpleTheme(new Uri("avares://ControlCatalog/Styles")); + public static SimpleTheme Simple = new SimpleTheme(new Uri("avares://ControlCatalog/Styles")); - public static Styles DefaultLight = new Styles + public static Styles SimpleLight = new Styles { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { @@ -56,10 +56,10 @@ namespace ControlCatalog { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") }, - Default + Simple }; - public static Styles DefaultDark = new Styles + public static Styles SimpleDark = new Styles { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { @@ -73,7 +73,7 @@ namespace ControlCatalog { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") }, - Default + Simple }; public override void Initialize() diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 8358fb3cd4..2654574a3e 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,7 +29,7 @@ - + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7461e78c33..7f5a191519 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -187,8 +187,8 @@ FluentLight FluentDark - DefaultLight - DefaultDark + SimpleLight + SimpleDark ? items) { items ??= Array.Empty(); - 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(); 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)); + } + } } } diff --git a/samples/PlatformSanityChecks/App.xaml b/samples/PlatformSanityChecks/App.xaml index 25bab6ae35..1b9d64fca6 100644 --- a/samples/PlatformSanityChecks/App.xaml +++ b/samples/PlatformSanityChecks/App.xaml @@ -1,6 +1,5 @@ - - + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 4f7f06b529..5c743aabdb 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml index 6bae1955af..1b9d64fca6 100644 --- a/samples/Previewer/App.xaml +++ b/samples/Previewer/App.xaml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 98560e9ab0..2cc84168dc 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,7 +10,7 @@ - + diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml index 3ad1dce794..eb5f0e4dca 100644 --- a/samples/VirtualizationDemo/App.xaml +++ b/samples/VirtualizationDemo/App.xaml @@ -1,9 +1,7 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index bd6054327f..b27cfe77e8 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml index d9630eef58..e6d77dfaf4 100644 --- a/samples/interop/Direct3DInteropSample/App.paml +++ b/samples/interop/Direct3DInteropSample/App.paml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 81b94b4d09..f9ef4693d5 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 50581d47b1..a9b2e16d43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/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 SaveBookmark() + public Task 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> 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(); + } + + 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 OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2a7f3360ad..cae7a8fe75 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/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]) { diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 97df87d3d9..7ab67ea34d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/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(firstTextSourceIndex, length, currentProperties); } + + private static int CoerceLength(ReadOnlySlice 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; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index df83ada34a..a49e4ef13b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/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(); - 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; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index ab72601c3e..42a9e61c36 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, + internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); @@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice 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 /// /// protected static bool TryGetShapeableLength( - ReadOnlySlice text, - Typeface typeface, + ReadOnlySlice 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 && diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f3e8b5969c..0828b6518a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting /// /// The collapsing width. /// The . - private TextCollapsingProperties GetCollapsingProperties(double width) + private TextCollapsingProperties? GetCollapsingProperties(double width) { + if(_textTrimming == TextTrimming.None) + { + return null; + } + return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index c8a23097db..61b24dc8c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting /// /// A value that represents a collapsed line that can be displayed. /// - public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList); /// /// Create a justified line based on justification text properties. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f3c62f4994..fa1ab6fd29 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting } /// - 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 { 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 { 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 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); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 56a90f31ea..ab17263806 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -224,7 +224,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } /// - /// Returns if is between + /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5af02219ce..cf21e9b8b5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile return Task.FromResult(null); } - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(_fileInfo.OpenRead()); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(_fileInfo.OpenWrite()); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_fileInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 7267017eaf..cd6c8be1ae 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/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(null); } - public virtual Task SaveBookmark() + public Task> GetItemsAsync() + { + var items = _directoryInfo.GetDirectories() + .Select(d => (IStorageItem)new BclStorageFolder(d)) + .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .ToArray(); + + return Task.FromResult>(items); + } + + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_directoryInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index d21c950862..40f2720ee8 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/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] diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 965caf8216..46aa6efa72 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// - Task OpenRead(); + Task OpenReadAsync(); /// /// Returns true, if file is writeable. @@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// - Task OpenWrite(); + Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 25b9f01a92..0ffb9f41c6 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/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 { + /// + /// Gets the files and subfolders in the current folder. + /// + /// + /// 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 implementation object. + /// + Task> GetItemsAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 8513ebc7d9..f5469d31c9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable /// /// Returns identifier of a bookmark. Can be null if OS denied request. /// - Task SaveBookmark(); + Task SaveBookmarkAsync(); /// /// Gets the parent folder of the current storage item. diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..a6db4330a3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -1,12 +1,9 @@ // ReSharper disable InconsistentNaming // ReSharper disable CheckNamespace -using System; -using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; // Special license applies License.md @@ -16,10 +13,10 @@ namespace Avalonia.Rendering.Composition.Animations /// This is the base class for ExpressionAnimation and KeyFrameAnimation. /// /// - /// Use the method to start the animation. + /// Use the method to start the animation. /// Value parameters (as opposed to reference parameters which are set using ) /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. - /// Changing the value of the variable after is called will not affect + /// Changing the value of the variable after is called will not affect /// the value of the ExpressionAnimation. /// See the remarks section of ExpressionAnimation for additional information. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index 577910d975..ec2972044e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -16,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations /// This contrasts 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 method to start the animation. + /// Use the method to start the animation. /// public class ExpressionAnimation : CompositionAnimation { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs index 4692fde5e3..d21a4d06e3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs +++ b/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. /// public AnimationDelayBehavior DelayBehavior { get; set; } - + /// - /// Delay before the animation starts after is called. + /// Delay before the animation starts after is called. /// public System.TimeSpan DelayTime { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..cf53b86fa7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..1d85e95835 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; - using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +15,7 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IBrush foreground, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..0af8ba2752 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 549c1fd7de..5fd200ddff 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) : base(Rect.Empty, Matrix.Identity, aux) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index a9d1bf96e5..f2ffd7411c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IBrush? brush, diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 3d5be806e1..d381979c1e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -255,7 +255,7 @@ namespace Avalonia.Utilities /// /// Clamps a value between a minimum and maximum value. /// - /// The value. + /// The value. /// The minimum value. /// The maximum value. /// The clamped value. diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 39a83c00c4..5915388822 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -326,8 +326,8 @@ namespace Avalonia.Build.Tasks var op = i[c].Operand as MethodReference; // TODO: Throw an error - // This usually happens when same XAML resource was added twice for some weird reason - // We currently support it for dual-named default theme resource + // This usually happens when the same XAML resource was added twice for some weird reason + // We currently support it for dual-named default theme resources if (op != null && op.Name == TrampolineName) { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml deleted file mode 100644 index 35cd7a9faa..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml deleted file mode 100644 index db1fa3ee4e..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 907b00dfff..74a1df4991 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,9 +3,6 @@ xmlns:controls="using:Avalonia.Controls" x:CompileBindings="True"> - - 5,5,0,0 - @@ -25,7 +22,7 @@ Padding="0,0,10,0" UseLayoutRounding="False"> - @@ -45,7 +42,7 @@ - + + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + + + 5,5,0,0 + + @@ -81,10 +83,4 @@ - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 993745b1e5..59cc48975f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/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 + + 3 @@ -97,7 +99,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top" Background="{DynamicResource SystemControlBackgroundBaseLowBrush}" - CornerRadius="{TemplateBinding CornerRadius}" /> + CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" /> + - - - 80 - 40 - + + 80 + 40 - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml new file mode 100644 index 0000000000..9aa2dcd9f9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml similarity index 69% rename from src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml index 0e57f6b483..0e137c89c6 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml @@ -1,9 +1,10 @@ - + - - - - + + + - - - + + + - - - - - + + + + + - + - - - + + + - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml new file mode 100644 index 0000000000..1e507a91fe --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml similarity index 99% rename from src/Avalonia.Controls.DataGrid/Themes/Default.xaml rename to src/Avalonia.Controls.DataGrid/Themes/Simple.xaml index 83d9332613..6a748f399e 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml @@ -223,7 +223,7 @@ - @@ -270,7 +270,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Foreground="{TemplateBinding Foreground}" - Theme="{StaticResource DefaultDataGridRowGroupExpanderButtonTheme}" /> + Theme="{StaticResource SimpleDataGridRowGroupExpanderButtonTheme}" /> 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); } + /// + /// Closes the button's flyout. + /// + protected virtual void CloseFlyout() + { + Flyout?.Hide(); + } + /// /// Invoked when the button's flyout is opened. /// @@ -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(); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 1504d2b25f..00ebcab70e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/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); - } - /// /// Defines the property /// public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), - x => x.IsOpen); + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); /// /// Defines the property @@ -43,6 +38,14 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.RegisterDirect(nameof(ShowMode), x => x.ShowMode, (x, v) => x.ShowMode = v); + /// + /// Defines the property + /// + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); + /// /// Defines the AttachedFlyout property /// @@ -57,6 +60,12 @@ namespace Avalonia.Controls.Primitives private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _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); } + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. + /// + public IInputElement? OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; + event Action? 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) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e463bc5731..e540f58195 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/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>? textStyleOverrides = null; - - if (length > 0) + + if (length > 0 && SelectionForegroundBrush != null) { textStyleOverrides = new[] { new ValueSpan(start, length, new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: SelectionForegroundBrush ?? Brushes.White)) + foregroundBrush: SelectionForegroundBrush)) }; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1501d97470..3573ad9aaa 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/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(() => { diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 56b9caf085..91114628ee 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -119,14 +119,12 @@ namespace Avalonia.Controls nameof(Percentage), o => o.Percentage); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateStartingOffset), p => p.IndeterminateStartingOffset, (p, o) => p.IndeterminateStartingOffset = o); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateEndingOffset), @@ -139,14 +137,12 @@ namespace Avalonia.Controls private set { SetAndRaise(PercentageProperty, ref _percentage, value); } } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateStartingOffset { get => _indeterminateStartingOffset; set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateEndingOffset { get => _indeterminateEndingOffset; diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 0c8b1d125d..1f8abbc30d 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -44,8 +44,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty InlinesProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( nameof(Inlines)); public static readonly DirectProperty CanCopyProperty = @@ -138,7 +138,7 @@ namespace Avalonia.Controls /// Gets or sets the inlines. /// [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; /// /// 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(); 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)) { diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1b268db2f7..4c9e9327d4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/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; } } } diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index adddf3f57b..d719135a7f 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 004518598c..1270dbaa62 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" xmlns:diag="clr-namespace:Avalonia.Diagnostics" - xmlns:default="using:Avalonia.Themes.Default" Title="Avalonia DevTools" x:Class="Avalonia.Diagnostics.Views.MainWindow" Theme="{StaticResource {x:Type Window}}"> @@ -11,8 +10,8 @@ - - + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 96dc929434..c81997f2cb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -11,7 +11,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml deleted file mode 100644 index 8f5bea557c..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs deleted file mode 100644 index 598b418977..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Styling; - -namespace Avalonia.Themes.Default -{ - /// - /// The default Avalonia theme. - /// - public class DefaultTheme : Styles - { - } -} diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 855dc5363e..aa55065f6d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -41,13 +41,31 @@ M0,9 L10,0 20,9 19,10 10,2 1,10 z M0,1 L10,10 20,1 19,0 10,8 1,0 z - - + - + + + + + + + + + @@ -83,7 +101,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" @@ -99,7 +116,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" diff --git a/src/Avalonia.Themes.Default/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/Base.xaml rename to src/Avalonia.Themes.Simple/Accents/Base.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseDark.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseDark.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseLight.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseLight.xaml diff --git a/src/Avalonia.Themes.Default/ApiCompatBaseline.txt b/src/Avalonia.Themes.Simple/ApiCompatBaseline.txt similarity index 100% rename from src/Avalonia.Themes.Default/ApiCompatBaseline.txt rename to src/Avalonia.Themes.Simple/ApiCompatBaseline.txt diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj similarity index 100% rename from src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj rename to src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj diff --git a/src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml rename to src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Simple/Controls/Button.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/Button.xaml rename to src/Avalonia.Themes.Simple/Controls/Button.xaml diff --git a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml similarity index 94% rename from src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml rename to src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml index 4585fc8e56..9798e5290b 100644 --- a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - @@ -38,7 +38,7 @@ IsVisible="{TemplateBinding ShowButtonSpinner}" Rows="2"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> - @@ -44,7 +44,7 @@ TextElement.FontSize="10"> /// The base URL for the XAML context. - public SimpleTheme(Uri baseUri) + public SimpleTheme(Uri? baseUri = null) { - _baseUri = baseUri; + _baseUri = baseUri ?? new Uri("avares://Avalonia.Themes.Simple/"); InitStyles(_baseUri); } @@ -138,18 +138,18 @@ namespace Avalonia.Themes.Default { new StyleInclude(baseUri) { - Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") + Source = new Uri("avares://Avalonia.Themes.Simple/Controls/SimpleControls.xaml") }, new StyleInclude(baseUri) { - Source = new Uri("avares://Avalonia.Themes.Default/Accents/Base.xaml") + Source = new Uri("avares://Avalonia.Themes.Simple/Accents/Base.xaml") } }; _simpleLight = new Styles { new StyleInclude(baseUri) { - Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml") + Source = new Uri("avares://Avalonia.Themes.Simple/Accents/BaseLight.xaml") } }; @@ -157,7 +157,7 @@ namespace Avalonia.Themes.Default { new StyleInclude(baseUri) { - Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml") + Source = new Uri("avares://Avalonia.Themes.Simple/Accents/BaseDark.xaml") } }; } diff --git a/src/Avalonia.Themes.Default/SimpleThemeMode.cs b/src/Avalonia.Themes.Simple/SimpleThemeMode.cs similarity index 67% rename from src/Avalonia.Themes.Default/SimpleThemeMode.cs rename to src/Avalonia.Themes.Simple/SimpleThemeMode.cs index be33466327..683c751f10 100644 --- a/src/Avalonia.Themes.Default/SimpleThemeMode.cs +++ b/src/Avalonia.Themes.Simple/SimpleThemeMode.cs @@ -1,4 +1,4 @@ -namespace Avalonia.Themes.Default +namespace Avalonia.Themes.Simple { public enum SimpleThemeMode { diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index a80f406989..6400d67fde 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -1,11 +1,8 @@ using System; using System.IO; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 14dc53d7b5..2bc46e97b5 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/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 SaveBookmark() + public Task SaveBookmarkAsync() { return FileHandle.InvokeAsync("saveBookmark").AsTask(); } @@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage return Task.FromResult(null); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { return FileHandle.InvokeAsync("deleteBookmark").AsTask(); } @@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenRead => true; - public async Task OpenRead() + public async Task OpenReadAsync() { var stream = await FileHandle.InvokeAsync("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 OpenWrite() + public async Task OpenWriteAsync() { var properties = await FileHandle.InvokeAsync("getProperties"); var streamWriter = await FileHandle.InvokeAsync("openWrite"); @@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) { } + + public async Task> GetItemsAsync() + { + var items = await FileHandle.InvokeAsync("getItems"); + if (items is null) + { + return Array.Empty(); + } + + var count = items.Invoke("count"); + + return Enumerable.Range(0, count) + .Select(index => + { + var reference = items.Invoke("at", index); + return reference.Invoke("getKind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }; + }) + .Where(i => i is not null) + .ToArray()!; + } } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts index c32eef3226..aee74b9067 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts +++ b/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 { - var conn = window.indexedDB.open(this.databaseName, 1); + const conn = window.indexedDB.open(this.databaseName, 1); conn.onupgradeneeded = event => { const db = (>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 { await this.verityPermissions('read'); - var file = await this.handle.getFile(); - return file; + return await this.handle.getFile(); } public async openWrite(): Promise { @@ -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 { + 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 { 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 { // '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 { 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 { const options: SaveFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), suggestedName: (suggestedName || undefined), excludeAcceptAllOption, types: (types || undefined) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 7f1af46e97..81fa8c4bce 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -9,7 +8,6 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fb296d0e0..a801e83562 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/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(new IOSStorageFolder(Url.RemoveLastPathComponent())); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // no-op return Task.CompletedTask; } - public Task SaveBookmark() + public Task SaveBookmarkAsync() { try { @@ -102,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public bool CanOpenWrite => true; - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Write)); } @@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder public IOSStorageFolder(NSUrl url) : base(url) { } + + public Task> GetItemsAsync() + { + var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error); + if (error is not null) + { + return Task.FromException>(new NSErrorException(error)); + } + + var items = content + .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u)) + .ToArray(); + + return Task.FromResult>(items); + } } diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 1cf68c1605..f509bb21ba 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs index 5c3ac6adeb..4a879c8ced 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs @@ -10,9 +10,9 @@ public class UriExtensionsTests public void Assembly_Name_From_Query_Parsed() { const string key = "assembly"; - const string value = "Avalonia.Themes.Default"; + const string value = "Avalonia.Themes.Simple"; - var uri = new Uri($"resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?{key}={value}"); + var uri = new Uri($"resm:Avalonia.Themes.Simple.Accents.BaseLight.xaml?{key}={value}"); var name = uri.GetAssemblyNameFromQuery(); Assert.Equal(value, name); @@ -21,7 +21,7 @@ public class UriExtensionsTests [Fact] public void Assembly_Name_From_Empty_Query_Not_Parsed() { - var uri = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml"); + var uri = new Uri("resm:Avalonia.Themes.Simple.Accents.BaseLight.xaml"); var name = uri.GetAssemblyNameFromQuery(); Assert.Equal(string.Empty, name); diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 5d17808e0c..3f4978f544 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 9a5b49790d..86ba4c6005 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -4,6 +4,8 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Themes.Fluent; +using Avalonia.Themes.Simple; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -25,32 +27,25 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] - [Arguments("avares://Avalonia.Themes.Fluent/FluentDark.xaml")] - [Arguments("avares://Avalonia.Themes.Fluent/FluentLight.xaml")] - public bool InitFluentTheme(string themeUri) + [Arguments(FluentThemeMode.Dark)] + [Arguments(FluentThemeMode.Light)] + public bool InitFluentTheme(FluentThemeMode mode) { - UnitTestApplication.Current.Styles[0] = new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) + UnitTestApplication.Current.Styles[0] = new FluentTheme(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) { - Source = new Uri(themeUri) + Mode = mode }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _); } [Benchmark] - [Arguments("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")] - [Arguments("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml")] - public bool InitDefaultTheme(string themeUri) + [Arguments(SimpleThemeMode.Dark)] + [Arguments(SimpleThemeMode.Light)] + public bool InitSimpleTheme(SimpleThemeMode mode) { - UnitTestApplication.Current.Styles[0] = new Styles + UnitTestApplication.Current.Styles[0] = new SimpleTheme(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) - { - Source = new Uri(themeUri) - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) - { - Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") - } + Mode = mode }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs index eb4b88956d..c74f13b808 100644 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ b/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); + } + } } } diff --git a/tests/Avalonia.DesignerSupport.TestApp/App.xaml b/tests/Avalonia.DesignerSupport.TestApp/App.xaml index 5a33ffff80..ad32431b37 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/App.xaml +++ b/tests/Avalonia.DesignerSupport.TestApp/App.xaml @@ -1,10 +1,7 @@ - - - - - - + + + + diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index f66b2b0457..278b0e087e 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index 52acc78db1..4a90da77e7 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index a00b24bdd7..4572f7ae7c 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index a0efa7bdeb..f562529cb8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 29148e6f2e..af2435a52f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -466,7 +466,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - + "; var styles = AvaloniaRuntimeXamlLoader.Parse(xaml); diff --git a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf deleted file mode 100644 index 6d2ad86f94..0000000000 Binary files a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf and /dev/null differ diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf new file mode 100644 index 0000000000..79359c460b Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf differ diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index d3f2b44968..5e481f21c1 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 13cc14b03e..7d0cf05b0b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/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": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7d33f094fa..43948e9229 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/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(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().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]; + } + } } } diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 52ef23c966..cb6884cad8 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index c1be745aca..49da2794c1 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -6,7 +6,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.Rendering; using System.Reactive.Concurrency; using System.Collections.Generic; @@ -24,7 +24,7 @@ namespace Avalonia.UnitTests renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), styler: new Styler(), - theme: () => CreateDefaultTheme(), + theme: () => CreateSimpleTheme(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), fontManagerImpl: new MockFontManagerImpl(), textShaperImpl: new MockTextShaperImpl(), @@ -81,7 +81,7 @@ namespace Avalonia.UnitTests IScheduler scheduler = null, ICursorFactory standardCursorFactory = null, IStyler styler = null, - Func theme = null, + Func theme = null, IPlatformThreadingInterface threadingInterface = null, IFontManagerImpl fontManagerImpl = null, ITextShaperImpl textShaperImpl = null, @@ -122,7 +122,7 @@ namespace Avalonia.UnitTests public IScheduler Scheduler { get; } public ICursorFactory StandardCursorFactory { get; } public IStyler Styler { get; } - public Func Theme { get; } + public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } public IWindowImpl WindowImpl { get; } public IWindowingPlatform WindowingPlatform { get; } @@ -169,18 +169,9 @@ namespace Avalonia.UnitTests windowImpl: windowImpl ?? WindowImpl); } - private static Styles CreateDefaultTheme() + private static IStyle CreateSimpleTheme() { - var result = new Styles - { - new DefaultTheme(), - }; - - var baseLight = (IStyle)AvaloniaXamlLoader.Load( - new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")); - result.Add(baseLight); - - return result; + return new SimpleTheme { Mode = SimpleThemeMode.Light }; } private static IPlatformRenderInterface CreateRenderInterfaceMock() diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 63c2832b92..260771c9ab 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -71,12 +71,16 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); - var styles = Services.Theme?.Invoke(); + var theme = Services.Theme?.Invoke(); - if (styles != null) + if (theme is Styles styles) { Styles.AddRange(styles); } + else if (theme is not null) + { + Styles.Add(theme); + } } } }