Browse Source

Merge branch 'master' into fixes/refactoring/unused_namespace

pull/8645/head
Max Katz 4 years ago
committed by GitHub
parent
commit
6d99408461
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      azure-pipelines.yml
  2. 26
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 33
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  4. 2
      src/Avalonia.Base/Media/GlyphRun.cs
  5. 24
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  6. 17
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  7. 26
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  8. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  9. 2
      src/Avalonia.Base/Media/TextFormatting/TextLine.cs
  10. 402
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  11. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  12. 8
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  13. 16
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  14. 2
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  15. 4
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  16. 11
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  17. 2
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  18. 4
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
  19. 2
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
  20. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
  21. 2
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  22. 2
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  23. 2
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  24. 2
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  25. 2
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  26. 2
      src/Avalonia.Base/Utilities/MathUtilities.cs
  27. 23
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  28. 346
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  29. 94
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  30. 76
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  31. 20
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  32. 4
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  33. 56
      src/Avalonia.Controls/Button.cs
  34. 57
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  35. 7
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  36. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  37. 24
      src/Avalonia.Controls/RichTextBlock.cs
  38. 6
      src/Avalonia.Controls/TextBox.cs
  39. 33
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  40. 45
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts
  41. 25
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  42. 44
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  43. BIN
      tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf
  44. BIN
      tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf
  45. 8
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  46. 70
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

6
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

26
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

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

33
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

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

2
src/Avalonia.Base/Media/GlyphRun.cs

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

24
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

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

17
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

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

26
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

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

7
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

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

2
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

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

402
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

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

2
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

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

8
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

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

16
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

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

2
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]

4
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

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

11
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

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

2
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

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

4
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs

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

2
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs

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

4
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs

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

2
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

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

2
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

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

2
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

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

2
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

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

2
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

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

2
src/Avalonia.Base/Utilities/MathUtilities.cs

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

23
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

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

346
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

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

94
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

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

76
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

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

20
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

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

4
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

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

56
src/Avalonia.Controls/Button.cs

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

57
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

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

7
src/Avalonia.Controls/Presenters/TextPresenter.cs

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

2
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(() =>
{

24
src/Avalonia.Controls/RichTextBlock.cs

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

6
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;
}
}
}

33
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

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

45
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts

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

25
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

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

44
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);
}
}
}
}

BIN
tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf

Binary file not shown.

BIN
tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf

Binary file not shown.

8
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":
{

70
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

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

Loading…
Cancel
Save