152 changed files with 12320 additions and 4988 deletions
@ -1,35 +1,18 @@ |
|||||
using System; |
|
||||
using System.Reactive.Linq; |
|
||||
using Avalonia; |
|
||||
using Avalonia.Animation; |
|
||||
using Avalonia.Controls; |
using Avalonia.Controls; |
||||
using Avalonia.Data; |
|
||||
using Avalonia.Markup.Xaml; |
using Avalonia.Markup.Xaml; |
||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace RenderDemo.Pages |
namespace RenderDemo.Pages |
||||
{ |
{ |
||||
public class ClippingPage : UserControl |
public class ClippingPage : UserControl |
||||
{ |
{ |
||||
private Geometry _clip; |
|
||||
|
|
||||
public ClippingPage() |
public ClippingPage() |
||||
{ |
{ |
||||
InitializeComponent(); |
InitializeComponent(); |
||||
WireUpCheckbox(); |
|
||||
} |
} |
||||
|
|
||||
private void InitializeComponent() |
private void InitializeComponent() |
||||
{ |
{ |
||||
AvaloniaXamlLoader.Load(this); |
AvaloniaXamlLoader.Load(this); |
||||
} |
} |
||||
|
|
||||
private void WireUpCheckbox() |
|
||||
{ |
|
||||
var useMask = this.FindControl<CheckBox>("useMask"); |
|
||||
var clipped = this.FindControl<Border>("clipped"); |
|
||||
_clip = clipped.Clip; |
|
||||
useMask.Click += (s, e) => clipped.Clip = clipped.Clip == null ? _clip : null; |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,7 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
x:Class="RenderDemo.Pages.FormattedTextPage"> |
||||
|
</UserControl> |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System.Globalization; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace RenderDemo.Pages |
||||
|
{ |
||||
|
public class FormattedTextPage : UserControl |
||||
|
{ |
||||
|
public FormattedTextPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
public override void Render(DrawingContext context) |
||||
|
{ |
||||
|
const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor"; |
||||
|
|
||||
|
// Create the initial formatted text string.
|
||||
|
var formattedText = new FormattedText( |
||||
|
testString, |
||||
|
CultureInfo.GetCultureInfo("en-us"), |
||||
|
FlowDirection.LeftToRight, |
||||
|
new Typeface("Verdana"), |
||||
|
32, |
||||
|
Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 }; |
||||
|
|
||||
|
// Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
|
||||
|
|
||||
|
// Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
|
||||
|
// The font size is calculated in terms of points -- not as device-independent pixels.
|
||||
|
formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5); |
||||
|
|
||||
|
// Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
|
||||
|
formattedText.SetFontWeight(FontWeight.Bold, 6, 11); |
||||
|
|
||||
|
var gradient = new LinearGradientBrush |
||||
|
{ |
||||
|
GradientStops = |
||||
|
new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) }, |
||||
|
StartPoint = new RelativePoint(0,0, RelativeUnit.Relative), |
||||
|
EndPoint = new RelativePoint(0,1, RelativeUnit.Relative) |
||||
|
}; |
||||
|
|
||||
|
// Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
|
||||
|
formattedText.SetForegroundBrush(gradient, 6, 11); |
||||
|
|
||||
|
// Use an Italic font style beginning at the 28th character and continuing for 28 characters.
|
||||
|
formattedText.SetFontStyle(FontStyle.Italic, 28, 28); |
||||
|
|
||||
|
context.DrawText(formattedText, new Point(10, 0)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,18 +0,0 @@ |
|||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Shared.PlatformSupport |
|
||||
{ |
|
||||
internal partial class StandardRuntimePlatform |
|
||||
{ |
|
||||
public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo |
|
||||
{ |
|
||||
IsCoreClr = false, |
|
||||
IsDesktop = false, |
|
||||
IsMobile = true, |
|
||||
IsDotNetFramework = false, |
|
||||
IsMono = true, |
|
||||
IsUnix = true, |
|
||||
OperatingSystem = OperatingSystemType.Android |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,3 @@ |
|||||
|
Compat issues with assembly Avalonia.DesktopRuntime: |
||||
|
TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract. |
||||
|
Total Issues: 1 |
||||
@ -1,40 +0,0 @@ |
|||||
using System; |
|
||||
using System.Runtime.InteropServices; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
|
|
||||
namespace Avalonia.Shared.PlatformSupport |
|
||||
{ |
|
||||
internal partial class StandardRuntimePlatform |
|
||||
{ |
|
||||
private static readonly Lazy<RuntimePlatformInfo> Info = new Lazy<RuntimePlatformInfo>(() => |
|
||||
{ |
|
||||
OperatingSystemType os; |
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
|
||||
os = OperatingSystemType.OSX; |
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
|
||||
os = OperatingSystemType.Linux; |
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
|
||||
os = OperatingSystemType.WinNT; |
|
||||
else |
|
||||
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription); |
|
||||
|
|
||||
return new RuntimePlatformInfo |
|
||||
{ |
|
||||
#if NETCOREAPP2_0
|
|
||||
IsCoreClr = true, |
|
||||
#elif NET461
|
|
||||
IsDotNetFramework = false, |
|
||||
#endif
|
|
||||
IsDesktop = true, |
|
||||
IsMono = false, |
|
||||
IsMobile = false, |
|
||||
IsUnix = os != OperatingSystemType.WinNT, |
|
||||
OperatingSystem = os, |
|
||||
}; |
|
||||
}); |
|
||||
|
|
||||
|
|
||||
public RuntimePlatformInfo GetRuntimeInfo() => Info.Value; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,18 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFrameworks>net6.0;net461;netstandard2.0</TargetFrameworks> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="../Avalonia.Base/Avalonia.Base.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" Condition="'$(TargetFramework)' == 'net461'" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\build\NetCore.props" /> |
||||
|
<Import Project="..\..\build\NetFX.props" /> |
||||
|
<Import Project="..\..\build\NullableEnable.props" /> |
||||
|
</Project> |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System.Reflection; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
|
||||
|
namespace Avalonia.PlatformSupport |
||||
|
{ |
||||
|
public static class StandardRuntimePlatformServices |
||||
|
{ |
||||
|
public static void Register(Assembly? assembly = null) |
||||
|
{ |
||||
|
var standardPlatform = new StandardRuntimePlatform(); |
||||
|
var os = standardPlatform.GetRuntimeInfo().OperatingSystem; |
||||
|
|
||||
|
AssetLoader.RegisterResUriParsers(); |
||||
|
AvaloniaLocator.CurrentMutable |
||||
|
.Bind<IRuntimePlatform>().ToConstant(standardPlatform) |
||||
|
.Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly)) |
||||
|
.Bind<IDynamicLibraryLoader>().ToConstant( |
||||
|
os switch |
||||
|
{ |
||||
|
OperatingSystemType.WinNT => new Win32Loader(), |
||||
|
OperatingSystemType.OSX => new UnixLoader(), |
||||
|
OperatingSystemType.Linux => new UnixLoader(), |
||||
|
OperatingSystemType.Android => new UnixLoader(), |
||||
|
// iOS, WASM, ...
|
||||
|
_ => (IDynamicLibraryLoader)new NotSupportedLoader() |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
@ -1,29 +0,0 @@ |
|||||
namespace Avalonia.Media |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Stores information about a line of <see cref="FormattedText"/>.
|
|
||||
/// </summary>
|
|
||||
public class FormattedTextLine |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="FormattedTextLine"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="length">The length of the line, in characters.</param>
|
|
||||
/// <param name="height">The height of the line, in pixels.</param>
|
|
||||
public FormattedTextLine(int length, double height) |
|
||||
{ |
|
||||
Length = length; |
|
||||
Height = height; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the length of the line, in characters.
|
|
||||
/// </summary>
|
|
||||
public int Length { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the height of the line, in pixels.
|
|
||||
/// </summary>
|
|
||||
public double Height { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,39 +0,0 @@ |
|||||
namespace Avalonia.Media |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Describes the formatting for a span of text in a <see cref="FormattedText"/> object.
|
|
||||
/// </summary>
|
|
||||
public class FormattedTextStyleSpan |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="FormattedTextStyleSpan"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="startIndex">The index of the first character in the span.</param>
|
|
||||
/// <param name="length">The length of the span.</param>
|
|
||||
/// <param name="foregroundBrush">The span's foreground brush.</param>
|
|
||||
public FormattedTextStyleSpan( |
|
||||
int startIndex, |
|
||||
int length, |
|
||||
IBrush? foregroundBrush = null) |
|
||||
{ |
|
||||
StartIndex = startIndex; |
|
||||
Length = length; |
|
||||
ForegroundBrush = foregroundBrush; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the index of the first character in the span.
|
|
||||
/// </summary>
|
|
||||
public int StartIndex { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the length of the span.
|
|
||||
/// </summary>
|
|
||||
public int Length { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the span's foreground brush.
|
|
||||
/// </summary>
|
|
||||
public IBrush? ForegroundBrush { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,293 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
public sealed class ShapedBuffer : IList<GlyphInfo> |
||||
|
{ |
||||
|
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters(); |
||||
|
|
||||
|
public ShapedBuffer(ReadOnlySlice<char> text, int length, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) |
||||
|
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) |
||||
|
{ |
||||
|
Text = text; |
||||
|
GlyphInfos = glyphInfos; |
||||
|
GlyphTypeface = glyphTypeface; |
||||
|
FontRenderingEmSize = fontRenderingEmSize; |
||||
|
BidiLevel = bidiLevel; |
||||
|
} |
||||
|
|
||||
|
internal ArraySlice<GlyphInfo> GlyphInfos { get; } |
||||
|
|
||||
|
public ReadOnlySlice<char> Text { get; } |
||||
|
|
||||
|
public int Length => GlyphInfos.Length; |
||||
|
|
||||
|
public GlyphTypeface GlyphTypeface { get; } |
||||
|
|
||||
|
public double FontRenderingEmSize { get; } |
||||
|
|
||||
|
public sbyte BidiLevel { get; } |
||||
|
|
||||
|
public bool IsLeftToRight => (BidiLevel & 1) == 0; |
||||
|
|
||||
|
public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos); |
||||
|
|
||||
|
public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos); |
||||
|
|
||||
|
public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos); |
||||
|
|
||||
|
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Finds a glyph index for given character index.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterIndex">The character index.</param>
|
||||
|
/// <returns>
|
||||
|
/// The glyph index.
|
||||
|
/// </returns>
|
||||
|
private int FindGlyphIndex(int characterIndex) |
||||
|
{ |
||||
|
if (characterIndex < GlyphInfos[0].GlyphCluster) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster) |
||||
|
{ |
||||
|
return GlyphInfos.Length - 1; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
var comparer = s_clusterComparer; |
||||
|
|
||||
|
var clusters = GlyphInfos.Span; |
||||
|
|
||||
|
var searchValue = new GlyphInfo(0, characterIndex); |
||||
|
|
||||
|
var start = clusters.BinarySearch(searchValue, comparer); |
||||
|
|
||||
|
if (start < 0) |
||||
|
{ |
||||
|
while (characterIndex > 0 && start < 0) |
||||
|
{ |
||||
|
characterIndex--; |
||||
|
|
||||
|
searchValue = new GlyphInfo(0, characterIndex); |
||||
|
|
||||
|
start = clusters.BinarySearch(searchValue, comparer); |
||||
|
} |
||||
|
|
||||
|
if (start < 0) |
||||
|
{ |
||||
|
return -1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) |
||||
|
{ |
||||
|
start--; |
||||
|
} |
||||
|
|
||||
|
return start; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Splits the <see cref="TextRun"/> at specified length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The length.</param>
|
||||
|
/// <returns>The split result.</returns>
|
||||
|
internal SplitResult<ShapedBuffer> Split(int length) |
||||
|
{ |
||||
|
var glyphCount = FindGlyphIndex(Text.Start + length); |
||||
|
|
||||
|
if (Text.Length == length) |
||||
|
{ |
||||
|
return new SplitResult<ShapedBuffer>(this, null); |
||||
|
} |
||||
|
|
||||
|
if (Text.Length == glyphCount) |
||||
|
{ |
||||
|
return new SplitResult<ShapedBuffer>(this, null); |
||||
|
} |
||||
|
|
||||
|
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
||||
|
|
||||
|
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
||||
|
|
||||
|
return new SplitResult<ShapedBuffer>(first, second); |
||||
|
} |
||||
|
|
||||
|
int ICollection<GlyphInfo>.Count => throw new NotImplementedException(); |
||||
|
|
||||
|
bool ICollection<GlyphInfo>.IsReadOnly => true; |
||||
|
|
||||
|
public GlyphInfo this[int index] |
||||
|
{ |
||||
|
get => GlyphInfos[index]; |
||||
|
set => GlyphInfos[index] = value; |
||||
|
} |
||||
|
|
||||
|
int IList<GlyphInfo>.IndexOf(GlyphInfo item) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
void IList<GlyphInfo>.Insert(int index, GlyphInfo item) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
void IList<GlyphInfo>.RemoveAt(int index) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
void ICollection<GlyphInfo>.Add(GlyphInfo item) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
void ICollection<GlyphInfo>.Clear() |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
bool ICollection<GlyphInfo>.Contains(GlyphInfo item) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
bool ICollection<GlyphInfo>.Remove(GlyphInfo item) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator(); |
||||
|
|
||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
|
||||
|
private class CompareClusters : IComparer<GlyphInfo> |
||||
|
{ |
||||
|
private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default; |
||||
|
|
||||
|
public int Compare(GlyphInfo x, GlyphInfo y) |
||||
|
{ |
||||
|
return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly struct GlyphAdvanceList : IReadOnlyList<double> |
||||
|
{ |
||||
|
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
||||
|
|
||||
|
public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos) |
||||
|
{ |
||||
|
_glyphInfos = glyphInfos; |
||||
|
} |
||||
|
|
||||
|
public double this[int index] => _glyphInfos[index].GlyphAdvance; |
||||
|
|
||||
|
public int Count => _glyphInfos.Length; |
||||
|
|
||||
|
public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this); |
||||
|
|
||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
private readonly struct GlyphIndexList : IReadOnlyList<ushort> |
||||
|
{ |
||||
|
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
||||
|
|
||||
|
public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos) |
||||
|
{ |
||||
|
_glyphInfos = glyphInfos; |
||||
|
} |
||||
|
|
||||
|
public ushort this[int index] => _glyphInfos[index].GlyphIndex; |
||||
|
|
||||
|
public int Count => _glyphInfos.Length; |
||||
|
|
||||
|
public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this); |
||||
|
|
||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
private readonly struct GlyphClusterList : IReadOnlyList<int> |
||||
|
{ |
||||
|
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
||||
|
|
||||
|
public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos) |
||||
|
{ |
||||
|
_glyphInfos = glyphInfos; |
||||
|
} |
||||
|
|
||||
|
public int this[int index] => _glyphInfos[index].GlyphCluster; |
||||
|
|
||||
|
public int Count => _glyphInfos.Length; |
||||
|
|
||||
|
public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this); |
||||
|
|
||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
private readonly struct GlyphOffsetList : IReadOnlyList<Vector> |
||||
|
{ |
||||
|
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
||||
|
|
||||
|
public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos) |
||||
|
{ |
||||
|
_glyphInfos = glyphInfos; |
||||
|
} |
||||
|
|
||||
|
public Vector this[int index] => _glyphInfos[index].GlyphOffset; |
||||
|
|
||||
|
public int Count => _glyphInfos.Length; |
||||
|
|
||||
|
public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this); |
||||
|
|
||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public readonly struct GlyphInfo |
||||
|
{ |
||||
|
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) |
||||
|
{ |
||||
|
GlyphIndex = glyphIndex; |
||||
|
GlyphAdvance = glyphAdvance; |
||||
|
GlyphCluster = glyphCluster; |
||||
|
GlyphOffset = glyphOffset; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph index.
|
||||
|
/// </summary>
|
||||
|
public ushort GlyphIndex { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph cluster.
|
||||
|
/// </summary>
|
||||
|
public int GlyphCluster { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph advance.
|
||||
|
/// </summary>
|
||||
|
public double GlyphAdvance { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph offset.
|
||||
|
/// </summary>
|
||||
|
public Vector GlyphOffset { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal readonly struct SplitResult<T> |
||||
|
{ |
||||
|
public SplitResult(T first, T? second) |
||||
|
{ |
||||
|
First = first; |
||||
|
|
||||
|
Second = second; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the first part.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The first part.
|
||||
|
/// </value>
|
||||
|
public T First { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the second part.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The second part.
|
||||
|
/// </value>
|
||||
|
public T? Second { get; } |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,182 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// Ported from: https://github.com/SixLabors/Fonts/
|
||||
|
|
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a unicode string and all associated attributes
|
||||
|
/// for each character required for the bidirectional Unicode algorithm
|
||||
|
/// </summary>
|
||||
|
internal class BidiData |
||||
|
{ |
||||
|
private ArrayBuilder<BidiClass> _classes; |
||||
|
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes; |
||||
|
private ArrayBuilder<int> _pairedBracketValues; |
||||
|
private ArrayBuilder<BidiClass> _savedClasses; |
||||
|
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes; |
||||
|
private ArrayBuilder<sbyte> _tempLevelBuffer; |
||||
|
|
||||
|
public BidiData(sbyte paragraphEmbeddingLevel = 0) |
||||
|
{ |
||||
|
ParagraphEmbeddingLevel = paragraphEmbeddingLevel; |
||||
|
} |
||||
|
|
||||
|
public BidiData(ReadOnlySlice<char> text, sbyte paragraphEmbeddingLevel = 0) : this(paragraphEmbeddingLevel) |
||||
|
{ |
||||
|
Append(text); |
||||
|
} |
||||
|
|
||||
|
public sbyte ParagraphEmbeddingLevel { get; private set; } |
||||
|
|
||||
|
public bool HasBrackets { get; private set; } |
||||
|
|
||||
|
public bool HasEmbeddings { get; private set; } |
||||
|
|
||||
|
public bool HasIsolates { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the length of the data held by the BidiData
|
||||
|
/// </summary>
|
||||
|
public int Length{get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bidi character type of each code point
|
||||
|
/// </summary>
|
||||
|
public ArraySlice<BidiClass> Classes { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the paired bracket type for each code point
|
||||
|
/// </summary>
|
||||
|
public ArraySlice<BidiPairedBracketType> PairedBracketTypes { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the paired bracket value for code point
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The paired bracket values are the code points
|
||||
|
/// of each character where the opening code point
|
||||
|
/// is replaced with the closing code point for easier
|
||||
|
/// matching. Also, bracket code points are mapped
|
||||
|
/// to their canonical equivalents
|
||||
|
/// </remarks>
|
||||
|
public ArraySlice<int> PairedBracketValues { get; private set; } |
||||
|
|
||||
|
public void Append(ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
_classes.Add(text.Length); |
||||
|
_pairedBracketTypes.Add(text.Length); |
||||
|
_pairedBracketValues.Add(text.Length); |
||||
|
|
||||
|
var i = Length; |
||||
|
|
||||
|
var codePointEnumerator = new CodepointEnumerator(text); |
||||
|
|
||||
|
while (codePointEnumerator.MoveNext()) |
||||
|
{ |
||||
|
var codepoint = codePointEnumerator.Current; |
||||
|
|
||||
|
// Look up BiDiClass
|
||||
|
var dir = codepoint.BiDiClass; |
||||
|
|
||||
|
_classes[i] = dir; |
||||
|
|
||||
|
switch (dir) |
||||
|
{ |
||||
|
case BidiClass.LeftToRightEmbedding: |
||||
|
case BidiClass.LeftToRightOverride: |
||||
|
case BidiClass.RightToLeftEmbedding: |
||||
|
case BidiClass.RightToLeftOverride: |
||||
|
case BidiClass.PopDirectionalFormat: |
||||
|
{ |
||||
|
HasEmbeddings = true; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case BidiClass.LeftToRightIsolate: |
||||
|
case BidiClass.RightToLeftIsolate: |
||||
|
case BidiClass.FirstStrongIsolate: |
||||
|
case BidiClass.PopDirectionalIsolate: |
||||
|
{ |
||||
|
HasIsolates = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Lookup paired bracket types
|
||||
|
var pbt = codepoint.PairedBracketType; |
||||
|
|
||||
|
_pairedBracketTypes[i] = pbt; |
||||
|
|
||||
|
if (pbt == BidiPairedBracketType.Open) |
||||
|
{ |
||||
|
// Opening bracket types can never have a null pairing.
|
||||
|
codepoint.TryGetPairedBracket(out var paired); |
||||
|
|
||||
|
_pairedBracketValues[i] = Codepoint.GetCanonicalType(paired).Value; |
||||
|
|
||||
|
HasBrackets = true; |
||||
|
} |
||||
|
else if (pbt == BidiPairedBracketType.Close) |
||||
|
{ |
||||
|
_pairedBracketValues[i] = Codepoint.GetCanonicalType(codepoint).Value; |
||||
|
|
||||
|
HasBrackets = true; |
||||
|
} |
||||
|
|
||||
|
i++; |
||||
|
} |
||||
|
|
||||
|
Length = i; |
||||
|
|
||||
|
Classes = _classes.AsSlice(0, Length); |
||||
|
PairedBracketTypes = _pairedBracketTypes.AsSlice(0, Length); |
||||
|
PairedBracketValues = _pairedBracketValues.AsSlice(0, Length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Save the Types and PairedBracketTypes of this BiDiData
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This is used when processing embedded style runs with
|
||||
|
/// BiDiClass overrides. Text layout process saves the data,
|
||||
|
/// overrides the style runs to neutral, processes the bidi
|
||||
|
/// data for the entire paragraph and then restores this data
|
||||
|
/// before processing the embedded runs.
|
||||
|
/// </remarks>
|
||||
|
public void SaveTypes() |
||||
|
{ |
||||
|
// Capture the types data
|
||||
|
_savedClasses.Clear(); |
||||
|
_savedClasses.Add(_classes.AsSlice()); |
||||
|
_savedPairedBracketTypes.Clear(); |
||||
|
_savedPairedBracketTypes.Add(_pairedBracketTypes.AsSlice()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Restore the data saved by SaveTypes
|
||||
|
/// </summary>
|
||||
|
public void RestoreTypes() |
||||
|
{ |
||||
|
_classes.Clear(); |
||||
|
_classes.Add(_savedClasses.AsSlice()); |
||||
|
_pairedBracketTypes.Clear(); |
||||
|
_pairedBracketTypes.Add(_savedPairedBracketTypes.AsSlice()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a temporary level buffer. Used by the text layout process when
|
||||
|
/// resolving style runs with different BiDiClass.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">Length of the required ExpandableBuffer</param>
|
||||
|
/// <returns>An uninitialized level ExpandableBuffer</returns>
|
||||
|
public ArraySlice<sbyte> GetTempLevelBuffer(int length) |
||||
|
{ |
||||
|
_tempLevelBuffer.Clear(); |
||||
|
|
||||
|
return _tempLevelBuffer.Add(length, false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum BidiPairedBracketType |
||||
|
{ |
||||
|
None, //n
|
||||
|
Close, //c
|
||||
|
Open, //o
|
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,38 @@ |
|||||
|
using Avalonia.Media.TextFormatting; |
||||
|
|
||||
namespace Avalonia.Media |
namespace Avalonia.Media |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Holds a hit test result from a <see cref="FormattedText"/>.
|
/// Holds a hit test result from a <see cref="TextLayout"/>.
|
||||
/// </summary>
|
/// </summary>
|
||||
public class TextHitTestResult |
public readonly struct TextHitTestResult |
||||
{ |
{ |
||||
|
public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing) |
||||
|
{ |
||||
|
CharacterHit = characterHit; |
||||
|
TextPosition = textPosition; |
||||
|
IsInside = isInside; |
||||
|
IsTrailing = isTrailing; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the character hit of the hit test result.
|
||||
|
/// </summary>
|
||||
|
public CharacterHit CharacterHit { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets or sets a value indicating whether the point is inside the bounds of the text.
|
/// Gets a value indicating whether the point is inside the bounds of the text.
|
||||
/// </summary>
|
/// </summary>
|
||||
public bool IsInside { get; set; } |
public bool IsInside { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the index of the hit character in the text.
|
/// Gets the index of the hit character in the text.
|
||||
/// </summary>
|
/// </summary>
|
||||
public int TextPosition { get; set; } |
public int TextPosition { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets a value indicating whether the hit is on the trailing edge of the character.
|
/// Gets a value indicating whether the hit is on the trailing edge of the character.
|
||||
/// </summary>
|
/// </summary>
|
||||
public bool IsTrailing { get; set; } |
public bool IsTrailing { get; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,59 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace Avalonia.Platform |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Defines the platform-specific interface for <see cref="FormattedText"/>.
|
|
||||
/// </summary>
|
|
||||
public interface IFormattedTextImpl |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets the constraint of the text.
|
|
||||
/// </summary>
|
|
||||
Size Constraint { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The measured bounds of the text.
|
|
||||
/// </summary>
|
|
||||
Rect Bounds{ get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text.
|
|
||||
/// </summary>
|
|
||||
string Text { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the lines in the text.
|
|
||||
/// </summary>
|
|
||||
/// <returns>
|
|
||||
/// A collection of <see cref="FormattedTextLine"/> objects.
|
|
||||
/// </returns>
|
|
||||
IEnumerable<FormattedTextLine> GetLines(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Hit tests a point in the text.
|
|
||||
/// </summary>
|
|
||||
/// <param name="point">The point.</param>
|
|
||||
/// <returns>
|
|
||||
/// A <see cref="TextHitTestResult"/> describing the result of the hit test.
|
|
||||
/// </returns>
|
|
||||
TextHitTestResult HitTestPoint(Point point); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the bounds rectangle that the specified character occupies.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">The index of the character.</param>
|
|
||||
/// <returns>The character bounds.</returns>
|
|
||||
Rect HitTestTextPosition(int index); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the bounds rectangles that the specified text range occupies.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">The index of the first character.</param>
|
|
||||
/// <param name="length">The number of characters in the text range.</param>
|
|
||||
/// <returns>The character bounds.</returns>
|
|
||||
IEnumerable<Rect> HitTestTextRange(int index, int length); |
|
||||
} |
|
||||
} |
|
||||
@ -1,89 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A node in the scene graph which represents a text draw.
|
|
||||
/// </summary>
|
|
||||
internal class TextNode : BrushDrawOperation |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="TextNode"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="transform">The transform.</param>
|
|
||||
/// <param name="foreground">The foreground brush.</param>
|
|
||||
/// <param name="origin">The draw origin.</param>
|
|
||||
/// <param name="text">The text to draw.</param>
|
|
||||
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
|
|
||||
public TextNode( |
|
||||
Matrix transform, |
|
||||
IBrush foreground, |
|
||||
Point origin, |
|
||||
IFormattedTextImpl text, |
|
||||
IDictionary<IVisual, Scene>? childScenes = null) |
|
||||
: base(text.Bounds.Translate(origin), transform) |
|
||||
{ |
|
||||
Transform = transform; |
|
||||
Foreground = foreground.ToImmutable(); |
|
||||
Origin = origin; |
|
||||
Text = text; |
|
||||
ChildScenes = childScenes; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the transform with which the node will be drawn.
|
|
||||
/// </summary>
|
|
||||
public Matrix Transform { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the foreground brush.
|
|
||||
/// </summary>
|
|
||||
public IBrush Foreground { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the draw origin.
|
|
||||
/// </summary>
|
|
||||
public Point Origin { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text to draw.
|
|
||||
/// </summary>
|
|
||||
public IFormattedTextImpl Text { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override IDictionary<IVisual, Scene>? ChildScenes { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override void Render(IDrawingContextImpl context) |
|
||||
{ |
|
||||
context.Transform = Transform; |
|
||||
context.DrawText(Foreground, Origin, Text); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Determines if this draw operation equals another.
|
|
||||
/// </summary>
|
|
||||
/// <param name="transform">The transform of the other draw operation.</param>
|
|
||||
/// <param name="foreground">The foreground of the other draw operation.</param>
|
|
||||
/// <param name="origin">The draw origin of the other draw operation.</param>
|
|
||||
/// <param name="text">The text of the other draw operation.</param>
|
|
||||
/// <returns>True if the draw operations are the same, otherwise false.</returns>
|
|
||||
/// <remarks>
|
|
||||
/// The properties of the other draw operation are passed in as arguments to prevent
|
|
||||
/// allocation of a not-yet-constructed draw operation object.
|
|
||||
/// </remarks>
|
|
||||
internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text) |
|
||||
{ |
|
||||
return transform == Transform && |
|
||||
Equals(foreground, Foreground) && |
|
||||
origin == Origin && |
|
||||
Equals(text, Text); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override bool HitTest(Point p) => Bounds.Contains(p); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,184 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// Ported from: https://github.com/SixLabors/Fonts/
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A helper type for avoiding allocations while building arrays.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of item contained in the array.</typeparam>
|
||||
|
internal struct ArrayBuilder<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private const int DefaultCapacity = 4; |
||||
|
private const int MaxCoreClrArrayLength = 0x7FeFFFFF; |
||||
|
|
||||
|
// Starts out null, initialized on first Add.
|
||||
|
private T[] _data; |
||||
|
private int _size; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the number of items in the array.
|
||||
|
/// </summary>
|
||||
|
public int Length |
||||
|
{ |
||||
|
get => _size; |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
if (value == _size) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (value > 0) |
||||
|
{ |
||||
|
EnsureCapacity(value); |
||||
|
|
||||
|
_size = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_size = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a reference to specified element of the array.
|
||||
|
/// </summary>
|
||||
|
/// <param name="index">The index of the element to return.</param>
|
||||
|
/// <returns>The <typeparamref name="T"/>.</returns>
|
||||
|
/// <exception cref="IndexOutOfRangeException">
|
||||
|
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
||||
|
/// </exception>
|
||||
|
public ref T this[int index] |
||||
|
{ |
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
get |
||||
|
{ |
||||
|
#if DEBUG
|
||||
|
if (index.CompareTo(0) < 0 || index.CompareTo(_size) > 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(index)); |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
return ref _data![index]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Appends a given number of empty items to the array returning
|
||||
|
/// the items as a slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of items in the slice.</param>
|
||||
|
/// <param name="clear">Whether to clear the new slice, Defaults to <see langword="true"/>.</param>
|
||||
|
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
||||
|
public ArraySlice<T> Add(int length, bool clear = true) |
||||
|
{ |
||||
|
var position = _size; |
||||
|
|
||||
|
// Expand the array.
|
||||
|
Length += length; |
||||
|
|
||||
|
var slice = AsSlice(position, Length - position); |
||||
|
|
||||
|
if (clear) |
||||
|
{ |
||||
|
slice.Span.Clear(); |
||||
|
} |
||||
|
|
||||
|
return slice; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Appends the slice to the array copying the data across.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">The array slice.</param>
|
||||
|
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
||||
|
public ArraySlice<T> Add(in ReadOnlySlice<T> value) |
||||
|
{ |
||||
|
var position = _size; |
||||
|
|
||||
|
// Expand the array.
|
||||
|
Length += value.Length; |
||||
|
|
||||
|
var slice = AsSlice(position, Length - position); |
||||
|
|
||||
|
value.Span.CopyTo(slice.Span); |
||||
|
|
||||
|
return slice; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the array.
|
||||
|
/// Allocated memory is left intact for future usage.
|
||||
|
/// </summary>
|
||||
|
public void Clear() |
||||
|
{ |
||||
|
// No need to actually clear since we're not allowing reference types.
|
||||
|
_size = 0; |
||||
|
} |
||||
|
|
||||
|
private void EnsureCapacity(int min) |
||||
|
{ |
||||
|
var length = _data?.Length ?? 0; |
||||
|
|
||||
|
if (length >= min) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Same expansion algorithm as List<T>.
|
||||
|
var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; |
||||
|
|
||||
|
if (newCapacity > MaxCoreClrArrayLength) |
||||
|
{ |
||||
|
newCapacity = MaxCoreClrArrayLength; |
||||
|
} |
||||
|
|
||||
|
if (newCapacity < min) |
||||
|
{ |
||||
|
newCapacity = (uint)min; |
||||
|
} |
||||
|
|
||||
|
var array = new T[newCapacity]; |
||||
|
|
||||
|
if (_size > 0) |
||||
|
{ |
||||
|
Array.Copy(_data!, array, _size); |
||||
|
} |
||||
|
|
||||
|
_data = array; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the current state of the array as a slice.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public ArraySlice<T> AsSlice() => AsSlice(Length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the current state of the array as a slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of items in the slice.</param>
|
||||
|
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public ArraySlice<T> AsSlice(int length) => new ArraySlice<T>(_data!, 0, length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the current state of the array as a slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The index at which to begin the slice.</param>
|
||||
|
/// <param name="length">The number of items in the slice.</param>
|
||||
|
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,197 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// Ported from: https://github.com/SixLabors/Fonts/
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// ArraySlice represents a contiguous region of arbitrary memory similar
|
||||
|
/// to <see cref="Memory{T}"/> and <see cref="Span{T}"/> though constrained
|
||||
|
/// to arrays.
|
||||
|
/// Unlike <see cref="Span{T}"/>, it is not a byref-like type.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of item contained in the slice.</typeparam>
|
||||
|
internal readonly struct ArraySlice<T> : IReadOnlyList<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets an empty <see cref="ArraySlice{T}"/>
|
||||
|
/// </summary>
|
||||
|
public static ArraySlice<T> Empty => new ArraySlice<T>(Array.Empty<T>()); |
||||
|
|
||||
|
private readonly T[] _data; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The underlying data buffer.</param>
|
||||
|
public ArraySlice(T[] data) |
||||
|
: this(data, 0, data.Length) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The underlying data buffer.</param>
|
||||
|
/// <param name="start">The offset position in the underlying buffer this slice was created from.</param>
|
||||
|
/// <param name="length">The number of items in the slice.</param>
|
||||
|
public ArraySlice(T[] data, int start, int length) |
||||
|
{ |
||||
|
#if DEBUG
|
||||
|
if (start.CompareTo(0) < 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(start)); |
||||
|
} |
||||
|
|
||||
|
if (length.CompareTo(data.Length) > 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
if ((start + length).CompareTo(data.Length) > 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(data)); |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
_data = data; |
||||
|
Start = start; |
||||
|
Length = length; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates whether this instance of <see cref="ArraySlice{T}"/> is Empty.
|
||||
|
/// </summary>
|
||||
|
public bool IsEmpty => Length == 0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the offset position in the underlying buffer this slice was created from.
|
||||
|
/// </summary>
|
||||
|
public int Start { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of items in the slice.
|
||||
|
/// </summary>
|
||||
|
public int Length { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="Span{T}"/> representing this slice.
|
||||
|
/// </summary>
|
||||
|
public Span<T> Span => new Span<T>(_data, Start, Length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a reference to specified element of the slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="index">The index of the element to return.</param>
|
||||
|
/// <returns>The <typeparamref name="T"/>.</returns>
|
||||
|
/// <exception cref="IndexOutOfRangeException">
|
||||
|
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
||||
|
/// </exception>
|
||||
|
public ref T this[int index] |
||||
|
{ |
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
get |
||||
|
{ |
||||
|
#if DEBUG
|
||||
|
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(index)); |
||||
|
} |
||||
|
#endif
|
||||
|
var i = index + Start; |
||||
|
|
||||
|
return ref _data[i]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
|
||||
|
/// </summary>
|
||||
|
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice) |
||||
|
{ |
||||
|
return new ReadOnlySlice<T>(slice._data).AsSlice(slice.Start, slice.Length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
|
||||
|
/// </summary>
|
||||
|
public static implicit operator ArraySlice<T>(T[] array) => new ArraySlice<T>(array, 0, array.Length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fills the contents of this slice with the given value.
|
||||
|
/// </summary>
|
||||
|
public void Fill(T value) => Span.Fill(value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Forms a slice out of the given slice, beginning at 'start', of given length
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The index at which to begin this slice.</param>
|
||||
|
/// <param name="length">The desired length for the slice (exclusive).</param>
|
||||
|
/// <exception cref="ArgumentOutOfRangeException">
|
||||
|
/// Thrown when the specified <paramref name="start"/> or end index is not in range (<0 or >Length).
|
||||
|
/// </exception>
|
||||
|
public ArraySlice<T> Slice(int start, int length) => new ArraySlice<T>(_data, start, length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a specified number of contiguous elements from the start of the slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of elements to return.</param>
|
||||
|
/// <returns>A <see cref="ArraySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
|
||||
|
public ArraySlice<T> Take(int length) |
||||
|
{ |
||||
|
if (IsEmpty) |
||||
|
{ |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new ArraySlice<T>(_data, Start, length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
|
||||
|
/// <returns>A <see cref="ArraySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
|
||||
|
public ArraySlice<T> Skip(int length) |
||||
|
{ |
||||
|
if (IsEmpty) |
||||
|
{ |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new ArraySlice<T>(_data, Start + length, Length - length); |
||||
|
} |
||||
|
|
||||
|
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator() => |
||||
|
new ImmutableReadOnlyListStructEnumerator<T>(this); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
T IReadOnlyList<T>.this[int index] => this[index]; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
int IReadOnlyCollection<T>.Count => Length; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
|
// not use this product except in compliance with the License. You may obtain
|
||||
|
// a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
|
// License for the specific language governing permissions and limitations
|
||||
|
// under the License.
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Extension methods for binary searching an IReadOnlyList collection
|
||||
|
/// </summary>
|
||||
|
internal static class BinarySearchExtension |
||||
|
{ |
||||
|
private static int GetMedian(int low, int hi) |
||||
|
{ |
||||
|
System.Diagnostics.Debug.Assert(low <= hi); |
||||
|
System.Diagnostics.Debug.Assert(hi - low >= 0, "Length overflow!"); |
||||
|
return low + (hi - low >> 1); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs a binary search on the entire contents of an IReadOnlyList
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The list element type</typeparam>
|
||||
|
/// <param name="list">The list to be searched</param>
|
||||
|
/// <param name="value">The value to search for</param>
|
||||
|
/// <param name="comparer">The comparer</param>
|
||||
|
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
|
||||
|
public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer) where T : IComparable |
||||
|
{ |
||||
|
return list.BinarySearch(0, list.Count, value, comparer); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs a binary search on a a subset of an IReadOnlyList
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The list element type</typeparam>
|
||||
|
/// <param name="list">The list to be searched</param>
|
||||
|
/// <param name="index">The start of the range to be searched</param>
|
||||
|
/// <param name="length">The length of the range to be searched</param>
|
||||
|
/// <param name="value">The value to search for</param>
|
||||
|
/// <param name="comparer">A comparer</param>
|
||||
|
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
|
||||
|
public static int BinarySearch<T>(this IReadOnlyList<T> list, int index, int length, T value, IComparer<T> comparer) |
||||
|
{ |
||||
|
// Based on this: https://referencesource.microsoft.com/#mscorlib/system/array.cs,957
|
||||
|
var lo = index; |
||||
|
var hi = index + length - 1; |
||||
|
while (lo <= hi) |
||||
|
{ |
||||
|
var i = GetMedian(lo, hi); |
||||
|
var c = comparer.Compare(list[i], value); |
||||
|
if (c == 0) |
||||
|
return i; |
||||
|
if (c < 0) |
||||
|
{ |
||||
|
lo = i + 1; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
hi = i - 1; |
||||
|
} |
||||
|
} |
||||
|
return ~lo; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,58 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// Ported from: https://github.com/SixLabors/Fonts/
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides a mapped view of an underlying slice, selecting arbitrary indices
|
||||
|
/// from the source array.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of item contained in the underlying array.</typeparam>
|
||||
|
internal readonly struct MappedArraySlice<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private readonly ArraySlice<T> _data; |
||||
|
private readonly ArraySlice<int> _map; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="MappedArraySlice{T}"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The data slice.</param>
|
||||
|
/// <param name="map">The map slice.</param>
|
||||
|
public MappedArraySlice(in ArraySlice<T> data, in ArraySlice<int> map) |
||||
|
{ |
||||
|
#if DEBUG
|
||||
|
if (map.Length.CompareTo(data.Length) > 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(map)); |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
_data = data; |
||||
|
_map = map; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of items in the map.
|
||||
|
/// </summary>
|
||||
|
public int Length => _map.Length; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a reference to specified element of the slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="index">The index of the element to return.</param>
|
||||
|
/// <returns>The <typeparamref name="T"/>.</returns>
|
||||
|
/// <exception cref="IndexOutOfRangeException">
|
||||
|
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
||||
|
/// </exception>
|
||||
|
public ref T this[int index] |
||||
|
{ |
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
get => ref _data[_map[index]]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,596 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
//+-----------------------------------------------------------------------
|
||||
|
//
|
||||
|
//
|
||||
|
//
|
||||
|
// Contents: Generic span types
|
||||
|
//
|
||||
|
// [As of this creation, C# has no real generic type system]
|
||||
|
//
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
internal class Span |
||||
|
{ |
||||
|
public readonly object? element; |
||||
|
public int length; |
||||
|
|
||||
|
public Span(object? element, int length) |
||||
|
{ |
||||
|
this.element = element; |
||||
|
this.length = length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// VECTOR: A series of spans
|
||||
|
/// </summary>
|
||||
|
internal class SpanVector : IEnumerable |
||||
|
{ |
||||
|
private static readonly Equals s_referenceEquals = object.ReferenceEquals; |
||||
|
private static readonly Equals s_equals = object.Equals; |
||||
|
|
||||
|
private FrugalStructList<Span> _spans; |
||||
|
|
||||
|
internal SpanVector( |
||||
|
object? defaultObject, |
||||
|
FrugalStructList<Span> spans = new FrugalStructList<Span>()) |
||||
|
{ |
||||
|
Default = defaultObject; |
||||
|
_spans = spans; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get enumerator to vector
|
||||
|
/// </summary>
|
||||
|
public IEnumerator GetEnumerator() |
||||
|
{ |
||||
|
return new SpanEnumerator(this); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Add a new span to vector
|
||||
|
/// </summary>
|
||||
|
private void Add(Span span) |
||||
|
{ |
||||
|
_spans.Add(span); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Delete n elements of vector
|
||||
|
/// </summary>
|
||||
|
internal virtual void Delete(int index, int count, ref SpanPosition latestPosition) |
||||
|
{ |
||||
|
DeleteInternal(index, count); |
||||
|
|
||||
|
if (index <= latestPosition.Index) |
||||
|
latestPosition = new SpanPosition(); |
||||
|
} |
||||
|
|
||||
|
private void DeleteInternal(int index, int count) |
||||
|
{ |
||||
|
// Do removes highest index to lowest to minimize the number
|
||||
|
// of array entries copied.
|
||||
|
for (var i = index + count - 1; i >= index; --i) |
||||
|
{ |
||||
|
_spans.RemoveAt(i); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Insert n elements to vector
|
||||
|
/// </summary>
|
||||
|
private void Insert(int index, int count) |
||||
|
{ |
||||
|
for (var c = 0; c < count; c++) |
||||
|
_spans.Insert(index, new Span(null, 0)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Finds the span that contains the specified character position.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cp">position to find</param>
|
||||
|
/// <param name="latestPosition">Position of the most recently accessed span (e.g., the current span
|
||||
|
/// of a SpanRider) for performance; FindSpan runs in O(1) time if the specified cp is in the same span
|
||||
|
/// or an adjacent span.</param>
|
||||
|
/// <param name="spanPosition">receives the index and first cp of the span that contains the specified
|
||||
|
/// position or, if the position is past the end of the vector, the index and cp just past the end of
|
||||
|
/// the last span.</param>
|
||||
|
/// <returns>Returns true if cp is in range or false if not.</returns>
|
||||
|
internal bool FindSpan(int cp, SpanPosition latestPosition, out SpanPosition spanPosition) |
||||
|
{ |
||||
|
Debug.Assert(cp >= 0); |
||||
|
|
||||
|
var spanCount = _spans.Count; |
||||
|
int spanIndex, spanCP; |
||||
|
|
||||
|
if (cp == 0) |
||||
|
{ |
||||
|
// CP zero always corresponds to span index zero
|
||||
|
spanIndex = 0; |
||||
|
spanCP = 0; |
||||
|
} |
||||
|
else if (cp >= latestPosition.Offset || cp * 2 < latestPosition.Offset) |
||||
|
{ |
||||
|
// One of the following is true:
|
||||
|
// 1. cp is after the latest position (the most recently accessed span)
|
||||
|
// 2. cp is closer to zero than to the latest position
|
||||
|
if (cp >= latestPosition.Offset) |
||||
|
{ |
||||
|
// case 1: scan forward from the latest position
|
||||
|
spanIndex = latestPosition.Index; |
||||
|
spanCP = latestPosition.Offset; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// case 2: scan forward from the start of the span vector
|
||||
|
spanIndex = 0; |
||||
|
spanCP = 0; |
||||
|
} |
||||
|
|
||||
|
// Scan forward until we find the Span that contains the specified CP or
|
||||
|
// reach the end of the SpanVector
|
||||
|
for (; spanIndex < spanCount; ++spanIndex) |
||||
|
{ |
||||
|
var spanLength = _spans[spanIndex].length; |
||||
|
|
||||
|
if (cp < spanCP + spanLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
spanCP += spanLength; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// The specified CP is before the latest position but closer to it than to zero;
|
||||
|
// therefore scan backwards from the latest position
|
||||
|
spanIndex = latestPosition.Index; |
||||
|
spanCP = latestPosition.Offset; |
||||
|
|
||||
|
while (spanCP > cp) |
||||
|
{ |
||||
|
Debug.Assert(spanIndex > 0); |
||||
|
spanCP -= _spans[--spanIndex].length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Return index and cp of span in out param.
|
||||
|
spanPosition = new SpanPosition(spanIndex, spanCP); |
||||
|
|
||||
|
// Return true if the span is in range.
|
||||
|
return spanIndex != spanCount; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set an element as a value to a character range
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Implementation of span element object must implement Object.Equals to
|
||||
|
/// avoid runtime reflection cost on equality check of nested-type object.
|
||||
|
/// </remarks>
|
||||
|
public void SetValue(int first, int length, object element) |
||||
|
{ |
||||
|
Set(first, length, element, SpanVector.s_equals, new SpanPosition()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set an element as a value to a character range; takes a SpanPosition of a recently accessed
|
||||
|
/// span for performance and returns a known valid SpanPosition
|
||||
|
/// </summary>
|
||||
|
public SpanPosition SetValue(int first, int length, object element, SpanPosition spanPosition) |
||||
|
{ |
||||
|
return Set(first, length, element, SpanVector.s_equals, spanPosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set an element as a reference to a character range
|
||||
|
/// </summary>
|
||||
|
public void SetReference(int first, int length, object element) |
||||
|
{ |
||||
|
Set(first, length, element, SpanVector.s_referenceEquals, new SpanPosition()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set an element as a reference to a character range; takes a SpanPosition of a recently accessed
|
||||
|
/// span for performance and returns a known valid SpanPosition
|
||||
|
/// </summary>
|
||||
|
public SpanPosition SetReference(int first, int length, object element, SpanPosition spanPosition) |
||||
|
{ |
||||
|
return Set(first, length, element, SpanVector.s_referenceEquals, spanPosition); |
||||
|
} |
||||
|
|
||||
|
private SpanPosition Set(int first, int length, object? element, Equals equals, SpanPosition spanPosition) |
||||
|
{ |
||||
|
var inRange = FindSpan(first, spanPosition, out spanPosition); |
||||
|
|
||||
|
// fs = index of first span partly or completely updated
|
||||
|
// fc = character index at start of fs
|
||||
|
var fs = spanPosition.Index; |
||||
|
var fc = spanPosition.Offset; |
||||
|
|
||||
|
// Find the span that contains the first affected cp
|
||||
|
if (!inRange) |
||||
|
{ |
||||
|
// The first cp is past the end of the last span
|
||||
|
if (fc < first) |
||||
|
{ |
||||
|
// Create default run up to first
|
||||
|
Add(new Span(Default, first - fc)); |
||||
|
} |
||||
|
|
||||
|
if (Count > 0 |
||||
|
&& equals(_spans[Count - 1].element, element)) |
||||
|
{ |
||||
|
// New Element matches end Element, just extend end Element
|
||||
|
_spans[Count - 1].length += length; |
||||
|
|
||||
|
// Make sure fs and fc still agree
|
||||
|
if (fs == Count) |
||||
|
{ |
||||
|
fc += length; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Add(new Span(element, length)); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Now find the last span affected by the update
|
||||
|
|
||||
|
var ls = fs; |
||||
|
var lc = fc; |
||||
|
while (ls < Count |
||||
|
&& lc + _spans[ls].length <= first + length) |
||||
|
{ |
||||
|
lc += _spans[ls].length; |
||||
|
ls++; |
||||
|
} |
||||
|
// ls = first span following update to remain unchanged in part or in whole
|
||||
|
// lc = character index at start of ls
|
||||
|
|
||||
|
// expand update region backwards to include existing Spans of identical
|
||||
|
// Element type
|
||||
|
|
||||
|
if (first == fc) |
||||
|
{ |
||||
|
// Item at [fs] is completely replaced. Check prior item
|
||||
|
|
||||
|
if (fs > 0 |
||||
|
&& equals(_spans[fs - 1].element, element)) |
||||
|
{ |
||||
|
// Expand update area over previous run of equal classification
|
||||
|
fs--; |
||||
|
fc -= _spans[fs].length; |
||||
|
first = fc; |
||||
|
length += _spans[fs].length; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Item at [fs] is partially replaced. Check if it is same as update
|
||||
|
if (equals(_spans[fs].element, element)) |
||||
|
{ |
||||
|
// Expand update area back to start of first affected equal valued run
|
||||
|
length = first + length - fc; |
||||
|
first = fc; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Expand update region forwards to include existing Spans of identical
|
||||
|
// Element type
|
||||
|
|
||||
|
if (ls < Count |
||||
|
&& equals(_spans[ls].element, element)) |
||||
|
{ |
||||
|
// Extend update region to end of existing split run
|
||||
|
|
||||
|
length = lc + _spans[ls].length - first; |
||||
|
lc += _spans[ls].length; |
||||
|
ls++; |
||||
|
} |
||||
|
|
||||
|
// If no old Spans remain beyond area affected by update, handle easily:
|
||||
|
|
||||
|
if (ls >= Count) |
||||
|
{ |
||||
|
// None of the old span list extended beyond the update region
|
||||
|
|
||||
|
if (fc < first) |
||||
|
{ |
||||
|
// Updated region leaves some of [fs]
|
||||
|
|
||||
|
if (Count != fs + 2) |
||||
|
{ |
||||
|
if (!Resize(fs + 2)) |
||||
|
throw new OutOfMemoryException(); |
||||
|
} |
||||
|
_spans[fs].length = first - fc; |
||||
|
_spans[fs + 1] = new Span(element, length); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Updated item replaces [fs]
|
||||
|
if (Count != fs + 1) |
||||
|
{ |
||||
|
if (!Resize(fs + 1)) |
||||
|
throw new OutOfMemoryException(); |
||||
|
} |
||||
|
_spans[fs] = new Span(element, length); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Record partial element type at end, if any
|
||||
|
|
||||
|
object? trailingElement = null; |
||||
|
var trailingLength = 0; |
||||
|
|
||||
|
if (first + length > lc) |
||||
|
{ |
||||
|
trailingElement = _spans[ls].element; |
||||
|
trailingLength = lc + _spans[ls].length - (first + length); |
||||
|
} |
||||
|
|
||||
|
// Calculate change in number of Spans
|
||||
|
|
||||
|
var spanDelta = 1 // The new span
|
||||
|
+ (first > fc ? 1 : 0) // part span at start
|
||||
|
- (ls - fs); // existing affected span count
|
||||
|
|
||||
|
// Note part span at end doesn't affect the calculation - the run may need
|
||||
|
// updating, but it doesn't need creating.
|
||||
|
|
||||
|
if (spanDelta < 0) |
||||
|
{ |
||||
|
DeleteInternal(fs + 1, -spanDelta); |
||||
|
} |
||||
|
else if (spanDelta > 0) |
||||
|
{ |
||||
|
Insert(fs + 1, spanDelta); |
||||
|
// Initialize inserted Spans
|
||||
|
for (var i = 0; i < spanDelta; i++) |
||||
|
{ |
||||
|
_spans[fs + 1 + i] = new Span(null, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Assign Element values
|
||||
|
|
||||
|
// Correct Length of split span before updated range
|
||||
|
|
||||
|
if (fc < first) |
||||
|
{ |
||||
|
_spans[fs].length = first - fc; |
||||
|
fs++; |
||||
|
fc = first; |
||||
|
} |
||||
|
|
||||
|
// Record Element type for updated range
|
||||
|
|
||||
|
_spans[fs] = new Span(element, length); |
||||
|
fs++; |
||||
|
fc += length; |
||||
|
|
||||
|
// Correct Length of split span following updated range
|
||||
|
|
||||
|
if (lc < first + length) |
||||
|
{ |
||||
|
_spans[fs] = new Span(trailingElement, trailingLength); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Return a known valid span position.
|
||||
|
return new SpanPosition(fs, fc); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Number of spans in vector
|
||||
|
/// </summary>
|
||||
|
public int Count |
||||
|
{ |
||||
|
get { return _spans.Count; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The default element of vector
|
||||
|
/// </summary>
|
||||
|
public object? Default { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Span accessor at nth element
|
||||
|
/// </summary>
|
||||
|
public Span this[int index] |
||||
|
{ |
||||
|
get { return _spans[index]; } |
||||
|
} |
||||
|
|
||||
|
private bool Resize(int targetCount) |
||||
|
{ |
||||
|
if (targetCount > Count) |
||||
|
{ |
||||
|
for (var c = 0; c < targetCount - Count; c++) |
||||
|
{ |
||||
|
_spans.Add(new Span(null, 0)); |
||||
|
} |
||||
|
} |
||||
|
else if (targetCount < Count) |
||||
|
{ |
||||
|
DeleteInternal(targetCount, Count - targetCount); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Equality check method
|
||||
|
/// </summary>
|
||||
|
internal delegate bool Equals(object? first, object? second); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// ENUMERATOR: To navigate a vector through its element
|
||||
|
/// </summary>
|
||||
|
internal sealed class SpanEnumerator : IEnumerator |
||||
|
{ |
||||
|
private readonly SpanVector _spans; |
||||
|
private int _current; // current span
|
||||
|
|
||||
|
internal SpanEnumerator(SpanVector spans) |
||||
|
{ |
||||
|
_spans = spans; |
||||
|
_current = -1; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The current span
|
||||
|
/// </summary>
|
||||
|
public object Current |
||||
|
{ |
||||
|
get { return _spans[_current]; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Move to the next span
|
||||
|
/// </summary>
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
_current++; |
||||
|
|
||||
|
return _current < _spans.Count; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reset the enumerator
|
||||
|
/// </summary>
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
_current = -1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a Span's position as a pair of related values: its index in the
|
||||
|
/// SpanVector its CP offset from the start of the SpanVector.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct SpanPosition |
||||
|
{ |
||||
|
internal SpanPosition(int spanIndex, int spanOffset) |
||||
|
{ |
||||
|
Index = spanIndex; |
||||
|
Offset = spanOffset; |
||||
|
} |
||||
|
|
||||
|
internal int Index { get; } |
||||
|
|
||||
|
internal int Offset { get; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// RIDER: To navigate a vector through character index
|
||||
|
/// </summary>
|
||||
|
internal struct SpanRider |
||||
|
{ |
||||
|
private readonly SpanVector _spans; // vector of spans
|
||||
|
private SpanPosition _spanPosition; // index and cp of current span
|
||||
|
|
||||
|
public SpanRider(SpanVector spans, SpanPosition latestPosition) : this(spans, latestPosition, latestPosition.Offset) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public SpanRider(SpanVector spans, SpanPosition latestPosition = new SpanPosition(), int cp = 0) |
||||
|
{ |
||||
|
_spans = spans; |
||||
|
_spanPosition = new SpanPosition(); |
||||
|
CurrentPosition = 0; |
||||
|
Length = 0; |
||||
|
At(latestPosition, cp); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Move rider to a given cp
|
||||
|
/// </summary>
|
||||
|
public bool At(int cp) |
||||
|
{ |
||||
|
return At(_spanPosition, cp); |
||||
|
} |
||||
|
|
||||
|
public bool At(SpanPosition latestPosition, int cp) |
||||
|
{ |
||||
|
var inRange = _spans.FindSpan(cp, latestPosition, out _spanPosition); |
||||
|
if (inRange) |
||||
|
{ |
||||
|
// cp is in range:
|
||||
|
// - Length is the distance to the end of the span
|
||||
|
// - CurrentPosition is cp
|
||||
|
Length = _spans[_spanPosition.Index].length - (cp - _spanPosition.Offset); |
||||
|
CurrentPosition = cp; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// cp is out of range:
|
||||
|
// - Length is the default span length
|
||||
|
// - CurrentPosition is the end of the last span
|
||||
|
Length = int.MaxValue; |
||||
|
CurrentPosition = _spanPosition.Offset; |
||||
|
} |
||||
|
|
||||
|
return inRange; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The first cp of the current span
|
||||
|
/// </summary>
|
||||
|
public int CurrentSpanStart |
||||
|
{ |
||||
|
get { return _spanPosition.Offset; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The length of current span start from the current cp
|
||||
|
/// </summary>
|
||||
|
public int Length { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The current position
|
||||
|
/// </summary>
|
||||
|
public int CurrentPosition { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The element of the current span
|
||||
|
/// </summary>
|
||||
|
public object? CurrentElement |
||||
|
{ |
||||
|
get { return _spanPosition.Index >= _spans.Count ? _spans.Default : _spans[_spanPosition.Index].element; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Index of the span at the current position.
|
||||
|
/// </summary>
|
||||
|
public int CurrentSpanIndex |
||||
|
{ |
||||
|
get { return _spanPosition.Index; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Index and first cp of the current span.
|
||||
|
/// </summary>
|
||||
|
public SpanPosition SpanPosition |
||||
|
{ |
||||
|
get { return _spanPosition; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,17 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|
||||
<PropertyGroup> |
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> |
|
||||
<HasSharedItems>true</HasSharedItems> |
|
||||
<SharedGUID>{E4D9629C-F168-4224-8F51-F5E482FFEC42}</SharedGUID> |
|
||||
</PropertyGroup> |
|
||||
<PropertyGroup Label="Configuration"> |
|
||||
<Import_RootNamespace>Avalonia.Shared.PlatformSupport</Import_RootNamespace> |
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<Compile Include="$(MSBuildThisFileDirectory)AssetLoader.cs" /> |
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DynLoader.cs" /> |
|
||||
<Compile Include="$(MSBuildThisFileDirectory)StandardRuntimePlatform.cs" /> |
|
||||
<Compile Include="$(MSBuildThisFileDirectory)StandardRuntimePlatformServices.cs" /> |
|
||||
</ItemGroup> |
|
||||
</Project> |
|
||||
@ -1,11 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|
||||
<PropertyGroup> |
|
||||
<ProjectGuid>{E4D9629C-F168-4224-3F51-A5E482FFBC42}</ProjectGuid> |
|
||||
</PropertyGroup> |
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> |
|
||||
<Import Project="PlatformSupport.projitems" Label="Shared" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> |
|
||||
</Project> |
|
||||
@ -1,28 +0,0 @@ |
|||||
using System.Reflection; |
|
||||
using System.Runtime.InteropServices; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Platform.Interop; |
|
||||
|
|
||||
namespace Avalonia.Shared.PlatformSupport |
|
||||
{ |
|
||||
static class StandardRuntimePlatformServices |
|
||||
{ |
|
||||
public static void Register(Assembly assembly = null) |
|
||||
{ |
|
||||
var standardPlatform = new StandardRuntimePlatform(); |
|
||||
AssetLoader.RegisterResUriParsers(); |
|
||||
AvaloniaLocator.CurrentMutable |
|
||||
.Bind<IRuntimePlatform>().ToConstant(standardPlatform) |
|
||||
.Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly)) |
|
||||
.Bind<IDynamicLibraryLoader>().ToConstant( |
|
||||
#if __IOS__
|
|
||||
new IOSLoader() |
|
||||
#else
|
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) |
|
||||
? (IDynamicLibraryLoader)new Win32Loader() |
|
||||
: new UnixLoader() |
|
||||
#endif
|
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.RenderHelpers |
|
||||
{ |
|
||||
static class QuadBezierHelper |
|
||||
{ |
|
||||
public static void QuadraticBezierTo(IStreamGeometryContextImpl context, Point current, Point controlPoint, Point endPoint) |
|
||||
{ |
|
||||
//(s, (s + 2c)/ 3, (e + 2c)/ 3, e)
|
|
||||
context.CubicBezierTo((current + 2*controlPoint)/3, (endPoint + 2*controlPoint)/3, endPoint); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|
||||
<PropertyGroup> |
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> |
|
||||
<HasSharedItems>true</HasSharedItems> |
|
||||
<SharedGUID>3c4c0cb4-0c0f-4450-a37b-148c84ff905f</SharedGUID> |
|
||||
</PropertyGroup> |
|
||||
<PropertyGroup Label="Configuration"> |
|
||||
<Import_RootNamespace>Avalonia.RenderHelpers</Import_RootNamespace> |
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<Compile Include="$(MSBuildThisFileDirectory)QuadBezierHelper.cs" /> |
|
||||
</ItemGroup> |
|
||||
</Project> |
|
||||
@ -1,13 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|
||||
<PropertyGroup Label="Globals"> |
|
||||
<ProjectGuid>3c4c0cb4-0c0f-4450-a37b-148c84ff905f</ProjectGuid> |
|
||||
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion> |
|
||||
</PropertyGroup> |
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> |
|
||||
<PropertyGroup /> |
|
||||
<Import Project="RenderHelpers.projitems" Label="Shared" /> |
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> |
|
||||
</Project> |
|
||||
@ -1,84 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Input.Raw; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia |
|
||||
{ |
|
||||
internal class ManagedWindowResizeDragHelper |
|
||||
{ |
|
||||
private readonly IWindowBaseImpl _window; |
|
||||
private readonly Action<bool> _captureMouse; |
|
||||
private readonly Action<Rect> _resize; |
|
||||
private WindowEdge? _edge; |
|
||||
private Point _prevPoint; |
|
||||
|
|
||||
public ManagedWindowResizeDragHelper(IWindowBaseImpl window, Action<bool> captureMouse, Action<Rect> resize = null) |
|
||||
{ |
|
||||
_window = window; |
|
||||
_captureMouse = captureMouse; |
|
||||
_resize = resize; |
|
||||
} |
|
||||
|
|
||||
public void BeginResizeDrag(WindowEdge edge, Point currentMousePosition) |
|
||||
{ |
|
||||
_captureMouse(true); |
|
||||
_prevPoint = currentMousePosition; |
|
||||
_edge = edge; |
|
||||
} |
|
||||
|
|
||||
public bool PreprocessInputEvent(ref RawInputEventArgs e) |
|
||||
{ |
|
||||
if (_edge == null) |
|
||||
return false; |
|
||||
if (e is RawMouseEventArgs args) |
|
||||
{ |
|
||||
if (args.Type == RawMouseEventType.LeftButtonUp) |
|
||||
{ |
|
||||
_edge = null; |
|
||||
_captureMouse(false); |
|
||||
} |
|
||||
if (args.Type == RawMouseEventType.Move) |
|
||||
{ |
|
||||
MoveWindow(args.Position); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
_edge = null; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private void MoveWindow(Point position) |
|
||||
{ |
|
||||
var diff = position - _prevPoint; |
|
||||
var edge = _edge.Value; |
|
||||
var rc = new Rect(_window.Position, _window.ClientSize); |
|
||||
if (edge == WindowEdge.East || edge == WindowEdge.NorthEast || edge == WindowEdge.SouthEast) |
|
||||
{ |
|
||||
rc = rc.WithWidth(rc.Width + diff.X); |
|
||||
_prevPoint = _prevPoint.WithX(position.X); |
|
||||
} |
|
||||
if (edge == WindowEdge.West || edge == WindowEdge.NorthWest || edge == WindowEdge.SouthWest) |
|
||||
rc = rc.WithX(rc.X + diff.X).WithWidth(rc.Width - diff.X); |
|
||||
if (edge == WindowEdge.South || edge == WindowEdge.SouthWest || edge == WindowEdge.SouthEast) |
|
||||
{ |
|
||||
rc = rc.WithHeight(rc.Height + diff.Y); |
|
||||
_prevPoint = _prevPoint.WithY(position.Y); |
|
||||
} |
|
||||
if (edge == WindowEdge.North || edge == WindowEdge.NorthWest || edge == WindowEdge.NorthEast) |
|
||||
rc = rc.WithY(rc.Y + diff.Y).WithHeight(rc.Height - diff.Y); |
|
||||
if (_resize != null) |
|
||||
_resize(rc); |
|
||||
else |
|
||||
{ |
|
||||
if (_window.Position != rc.Position) |
|
||||
_window.Position = rc.Position; |
|
||||
if (_window.ClientSize != rc.Size) |
|
||||
_window.Resize(rc.Size); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,838 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
using SkiaSharp; |
|
||||
|
|
||||
namespace Avalonia.Skia |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Skia formatted text implementation.
|
|
||||
/// </summary>
|
|
||||
internal class FormattedTextImpl : IFormattedTextImpl |
|
||||
{ |
|
||||
private static readonly ThreadLocal<SKTextBlobBuilder> t_builder = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder()); |
|
||||
|
|
||||
private const float MAX_LINE_WIDTH = 10000; |
|
||||
|
|
||||
private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes = |
|
||||
new List<KeyValuePair<FBrushRange, IBrush>>(); |
|
||||
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>(); |
|
||||
private readonly SKPaint _paint; |
|
||||
private readonly List<Rect> _rects = new List<Rect>(); |
|
||||
public string Text { get; } |
|
||||
private readonly TextWrapping _wrapping; |
|
||||
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); |
|
||||
private float _lineHeight = 0; |
|
||||
private float _lineOffset = 0; |
|
||||
private Rect _bounds; |
|
||||
private List<AvaloniaFormattedTextLine> _skiaLines; |
|
||||
private ReadOnlySlice<ushort> _glyphs; |
|
||||
private ReadOnlySlice<float> _advances; |
|
||||
|
|
||||
public FormattedTextImpl( |
|
||||
string text, |
|
||||
Typeface typeface, |
|
||||
double fontSize, |
|
||||
TextAlignment textAlignment, |
|
||||
TextWrapping wrapping, |
|
||||
Size constraint, |
|
||||
IReadOnlyList<FormattedTextStyleSpan> spans) |
|
||||
{ |
|
||||
Text = text ?? string.Empty; |
|
||||
|
|
||||
UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize); |
|
||||
|
|
||||
_paint = new SKPaint |
|
||||
{ |
|
||||
TextEncoding = SKTextEncoding.Utf16, |
|
||||
IsStroke = false, |
|
||||
IsAntialias = true, |
|
||||
LcdRenderText = true, |
|
||||
SubpixelText = true, |
|
||||
IsLinearText = true, |
|
||||
Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface, |
|
||||
TextSize = (float)fontSize, |
|
||||
TextAlign = textAlignment.ToSKTextAlign() |
|
||||
}; |
|
||||
|
|
||||
//currently Skia does not measure properly with Utf8 !!!
|
|
||||
//Paint.TextEncoding = SKTextEncoding.Utf8;
|
|
||||
|
|
||||
_wrapping = wrapping; |
|
||||
_constraint = constraint; |
|
||||
|
|
||||
if (spans != null) |
|
||||
{ |
|
||||
foreach (var span in spans) |
|
||||
{ |
|
||||
if (span.ForegroundBrush != null) |
|
||||
{ |
|
||||
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Rebuild(); |
|
||||
} |
|
||||
|
|
||||
public Size Constraint => _constraint; |
|
||||
|
|
||||
public Rect Bounds => _bounds; |
|
||||
|
|
||||
public IEnumerable<FormattedTextLine> GetLines() |
|
||||
{ |
|
||||
return _lines; |
|
||||
} |
|
||||
|
|
||||
public TextHitTestResult HitTestPoint(Point point) |
|
||||
{ |
|
||||
float y = (float)point.Y; |
|
||||
|
|
||||
AvaloniaFormattedTextLine line = default; |
|
||||
|
|
||||
float nextTop = 0; |
|
||||
|
|
||||
foreach(var currentLine in _skiaLines) |
|
||||
{ |
|
||||
if(currentLine.Top <= y) |
|
||||
{ |
|
||||
line = currentLine; |
|
||||
nextTop = currentLine.Top + currentLine.Height; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
nextTop = currentLine.Top; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (!line.Equals(default(AvaloniaFormattedTextLine))) |
|
||||
{ |
|
||||
var rects = GetRects(); |
|
||||
|
|
||||
for (int c = line.Start; c < line.Start + line.TextLength; c++) |
|
||||
{ |
|
||||
var rc = rects[c]; |
|
||||
if (rc.Contains(point)) |
|
||||
{ |
|
||||
return new TextHitTestResult |
|
||||
{ |
|
||||
IsInside = !(line.TextLength > line.Length), |
|
||||
TextPosition = c, |
|
||||
IsTrailing = (point.X - rc.X) > rc.Width / 2 |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
int offset = 0; |
|
||||
|
|
||||
if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0) |
|
||||
{ |
|
||||
offset = line.TextLength > line.Length ? |
|
||||
line.Length : (line.Length - 1); |
|
||||
} |
|
||||
|
|
||||
if (y < nextTop) |
|
||||
{ |
|
||||
return new TextHitTestResult |
|
||||
{ |
|
||||
IsInside = false, |
|
||||
TextPosition = line.Start + offset, |
|
||||
IsTrailing = Text.Length == (line.Start + offset + 1) |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); |
|
||||
|
|
||||
return new TextHitTestResult() |
|
||||
{ |
|
||||
IsInside = false, |
|
||||
IsTrailing = end, |
|
||||
TextPosition = end ? Text.Length - 1 : 0 |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
public Rect HitTestTextPosition(int index) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(Text)) |
|
||||
{ |
|
||||
var alignmentOffset = TransformX(0, 0, _paint.TextAlign); |
|
||||
return new Rect(alignmentOffset, 0, 0, _lineHeight); |
|
||||
} |
|
||||
var rects = GetRects(); |
|
||||
if (index >= Text.Length || index < 0) |
|
||||
{ |
|
||||
var r = rects.LastOrDefault(); |
|
||||
|
|
||||
var c = Text[Text.Length - 1]; |
|
||||
|
|
||||
switch (c) |
|
||||
{ |
|
||||
case '\n': |
|
||||
case '\r': |
|
||||
return new Rect(r.X, r.Y, 0, _lineHeight); |
|
||||
default: |
|
||||
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); |
|
||||
} |
|
||||
} |
|
||||
return rects[index]; |
|
||||
} |
|
||||
|
|
||||
public IEnumerable<Rect> HitTestTextRange(int index, int length) |
|
||||
{ |
|
||||
List<Rect> result = new List<Rect>(); |
|
||||
|
|
||||
var rects = GetRects(); |
|
||||
|
|
||||
int lastIndex = index + length - 1; |
|
||||
|
|
||||
foreach (var line in _skiaLines.Where(l => |
|
||||
(l.Start + l.Length) > index && |
|
||||
lastIndex >= l.Start && |
|
||||
!l.IsEmptyTrailingLine)) |
|
||||
{ |
|
||||
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0); |
|
||||
|
|
||||
double left = rects[line.Start > index ? line.Start : index].X; |
|
||||
double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right; |
|
||||
|
|
||||
result.Add(new Rect(left, line.Top, right - left, line.Height)); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
return Text; |
|
||||
} |
|
||||
|
|
||||
private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) |
|
||||
{ |
|
||||
if(length == 0) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var glyphs = _glyphs.Buffer.Span.Slice(start, length); |
|
||||
var advances = _advances.Buffer.Span.Slice(start, length); |
|
||||
var builder = t_builder.Value; |
|
||||
|
|
||||
var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0); |
|
||||
|
|
||||
buffer.SetGlyphs(glyphs); |
|
||||
|
|
||||
var positions = buffer.GetPositionSpan(); |
|
||||
|
|
||||
var pos = 0f; |
|
||||
|
|
||||
for (int i = 0; i < advances.Length; i++) |
|
||||
{ |
|
||||
positions[i] = pos; |
|
||||
|
|
||||
pos += advances[i]; |
|
||||
} |
|
||||
|
|
||||
var blob = builder.Build(); |
|
||||
|
|
||||
if(blob != null) |
|
||||
{ |
|
||||
canvas.DrawText(blob, x, y, paint); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
internal void Draw(DrawingContextImpl context, |
|
||||
SKCanvas canvas, |
|
||||
SKPoint origin, |
|
||||
DrawingContextImpl.PaintWrapper foreground, |
|
||||
bool canUseLcdRendering) |
|
||||
{ |
|
||||
/* TODO: This originated from Native code, it might be useful for debugging character positions as |
|
||||
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when |
|
||||
* not needed anymore. |
|
||||
|
|
||||
SkPaint dpaint; |
|
||||
ctx->Canvas->save(); |
|
||||
ctx->Canvas->translate(origin.fX, origin.fY); |
|
||||
for (int c = 0; c < Lines.size(); c++) |
|
||||
{ |
|
||||
dpaint.setARGB(255, 0, 0, 0); |
|
||||
SkRect rc; |
|
||||
rc.fLeft = 0; |
|
||||
rc.fTop = Lines[c].Top; |
|
||||
rc.fRight = Lines[c].Width; |
|
||||
rc.fBottom = rc.fTop + LineOffset; |
|
||||
ctx->Canvas->drawRect(rc, dpaint); |
|
||||
} |
|
||||
for (int c = 0; c < Length; c++) |
|
||||
{ |
|
||||
dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10); |
|
||||
dpaint.setStyle(SkPaint::kFill_Style); |
|
||||
ctx->Canvas->drawRect(Rects[c], dpaint); |
|
||||
} |
|
||||
ctx->Canvas->restore(); |
|
||||
*/ |
|
||||
using (var paint = _paint.Clone()) |
|
||||
{ |
|
||||
IDisposable currd = null; |
|
||||
var currentWrapper = foreground; |
|
||||
SKPaint currentPaint = null; |
|
||||
try |
|
||||
{ |
|
||||
ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering); |
|
||||
bool hasCusomFGBrushes = _foregroundBrushes.Any(); |
|
||||
|
|
||||
for (int c = 0; c < _skiaLines.Count; c++) |
|
||||
{ |
|
||||
AvaloniaFormattedTextLine line = _skiaLines[c]; |
|
||||
|
|
||||
float x = TransformX(origin.X, line.Width, paint.TextAlign); |
|
||||
|
|
||||
if (!hasCusomFGBrushes) |
|
||||
{ |
|
||||
DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
float currX = x; |
|
||||
float measure; |
|
||||
int len; |
|
||||
float factor; |
|
||||
|
|
||||
switch (paint.TextAlign) |
|
||||
{ |
|
||||
case SKTextAlign.Left: |
|
||||
factor = 0; |
|
||||
break; |
|
||||
case SKTextAlign.Center: |
|
||||
factor = 0.5f; |
|
||||
break; |
|
||||
case SKTextAlign.Right: |
|
||||
factor = 1; |
|
||||
break; |
|
||||
default: |
|
||||
throw new ArgumentOutOfRangeException(); |
|
||||
} |
|
||||
|
|
||||
currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; |
|
||||
|
|
||||
for (int i = line.Start; i < line.Start + line.Length;) |
|
||||
{ |
|
||||
var fb = GetNextForegroundBrush(ref line, i, out len); |
|
||||
|
|
||||
if (fb != null) |
|
||||
{ |
|
||||
//TODO: figure out how to get the brush size
|
|
||||
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, |
|
||||
new Size()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|
||||
currentWrapper = foreground; |
|
||||
} |
|
||||
|
|
||||
measure = MeasureText(i, len); |
|
||||
currX += measure * factor; |
|
||||
|
|
||||
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); |
|
||||
|
|
||||
DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint); |
|
||||
|
|
||||
i += len; |
|
||||
currX += measure * (1 - factor); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|
||||
currd?.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper, |
|
||||
ref IDisposable curr, SKPaint paint, bool canUseLcdRendering) |
|
||||
{ |
|
||||
if (current == wrapper.Paint) |
|
||||
return; |
|
||||
curr?.Dispose(); |
|
||||
curr = wrapper.ApplyTo(paint); |
|
||||
paint.LcdRenderText = canUseLcdRendering; |
|
||||
} |
|
||||
|
|
||||
private static bool IsBreakChar(char c) |
|
||||
{ |
|
||||
//white space or zero space whitespace
|
|
||||
return char.IsWhiteSpace(c) || c == '\u200B'; |
|
||||
} |
|
||||
|
|
||||
private static int LineBreak(string textInput, int textIndex, int stop, |
|
||||
SKPaint paint, float maxWidth, |
|
||||
out int trailingCount) |
|
||||
{ |
|
||||
int lengthBreak; |
|
||||
if (maxWidth == -1) |
|
||||
{ |
|
||||
lengthBreak = stop - textIndex; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
string subText = textInput.Substring(textIndex, stop - textIndex); |
|
||||
lengthBreak = (int)paint.BreakText(subText, maxWidth, out _); |
|
||||
} |
|
||||
|
|
||||
//Check for white space or line breakers before the lengthBreak
|
|
||||
int startIndex = textIndex; |
|
||||
int index = textIndex; |
|
||||
int word_start = textIndex; |
|
||||
bool prevBreak = true; |
|
||||
|
|
||||
trailingCount = 0; |
|
||||
|
|
||||
while (index < stop) |
|
||||
{ |
|
||||
int prevText = index; |
|
||||
char currChar = textInput[index++]; |
|
||||
bool currBreak = IsBreakChar(currChar); |
|
||||
|
|
||||
if (!currBreak && prevBreak) |
|
||||
{ |
|
||||
word_start = prevText; |
|
||||
} |
|
||||
|
|
||||
prevBreak = currBreak; |
|
||||
|
|
||||
if (index > startIndex + lengthBreak) |
|
||||
{ |
|
||||
if (currBreak) |
|
||||
{ |
|
||||
// eat the rest of the whitespace
|
|
||||
while (index < stop && IsBreakChar(textInput[index])) |
|
||||
{ |
|
||||
index++; |
|
||||
} |
|
||||
|
|
||||
trailingCount = index - prevText; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// backup until a whitespace (or 1 char)
|
|
||||
if (word_start == startIndex) |
|
||||
{ |
|
||||
if (prevText > startIndex) |
|
||||
{ |
|
||||
index = prevText; |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
index = word_start; |
|
||||
} |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if ('\n' == currChar) |
|
||||
{ |
|
||||
int ret = index - startIndex; |
|
||||
int lineBreakSize = 1; |
|
||||
if (index < stop) |
|
||||
{ |
|
||||
currChar = textInput[index++]; |
|
||||
if ('\r' == currChar) |
|
||||
{ |
|
||||
ret = index - startIndex; |
|
||||
++lineBreakSize; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
trailingCount = lineBreakSize; |
|
||||
|
|
||||
return ret; |
|
||||
} |
|
||||
|
|
||||
if ('\r' == currChar) |
|
||||
{ |
|
||||
int ret = index - startIndex; |
|
||||
int lineBreakSize = 1; |
|
||||
if (index < stop) |
|
||||
{ |
|
||||
currChar = textInput[index++]; |
|
||||
if ('\n' == currChar) |
|
||||
{ |
|
||||
ret = index - startIndex; |
|
||||
++lineBreakSize; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
trailingCount = lineBreakSize; |
|
||||
|
|
||||
return ret; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return index - startIndex; |
|
||||
} |
|
||||
|
|
||||
private void BuildRects() |
|
||||
{ |
|
||||
// Build character rects
|
|
||||
SKTextAlign align = _paint.TextAlign; |
|
||||
|
|
||||
for (int li = 0; li < _skiaLines.Count; li++) |
|
||||
{ |
|
||||
var line = _skiaLines[li]; |
|
||||
float prevRight = TransformX(0, line.Width, align); |
|
||||
double nextTop = line.Top + line.Height; |
|
||||
|
|
||||
if (li + 1 < _skiaLines.Count) |
|
||||
{ |
|
||||
nextTop = _skiaLines[li + 1].Top; |
|
||||
} |
|
||||
|
|
||||
for (int i = line.Start; i < line.Start + line.TextLength; i++) |
|
||||
{ |
|
||||
var w = line.IsEmptyTrailingLine ? 0 : _advances[i]; |
|
||||
|
|
||||
_rects.Add(new Rect( |
|
||||
prevRight, |
|
||||
line.Top, |
|
||||
w, |
|
||||
nextTop - line.Top)); |
|
||||
prevRight += w; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length) |
|
||||
{ |
|
||||
IBrush result = null; |
|
||||
int len = length = line.Start + line.Length - index; |
|
||||
|
|
||||
if (_foregroundBrushes.Any()) |
|
||||
{ |
|
||||
var bi = _foregroundBrushes.FindIndex(b => |
|
||||
b.Key.StartIndex <= index && |
|
||||
b.Key.EndIndex > index |
|
||||
); |
|
||||
|
|
||||
if (bi > -1) |
|
||||
{ |
|
||||
var match = _foregroundBrushes[bi]; |
|
||||
|
|
||||
len = match.Key.EndIndex - index; |
|
||||
result = match.Value; |
|
||||
|
|
||||
if (len > 0 && len < length) |
|
||||
{ |
|
||||
length = len; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
int endIndex = index + length; |
|
||||
int max = bi == -1 ? _foregroundBrushes.Count : bi; |
|
||||
var next = _foregroundBrushes.Take(max) |
|
||||
.Where(b => b.Key.StartIndex < endIndex && |
|
||||
b.Key.StartIndex > index) |
|
||||
.OrderBy(b => b.Key.StartIndex) |
|
||||
.FirstOrDefault(); |
|
||||
|
|
||||
if (next.Value != null) |
|
||||
{ |
|
||||
length = next.Key.StartIndex - index; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
private List<Rect> GetRects() |
|
||||
{ |
|
||||
if (Text.Length > _rects.Count) |
|
||||
{ |
|
||||
BuildRects(); |
|
||||
} |
|
||||
|
|
||||
return _rects; |
|
||||
} |
|
||||
|
|
||||
private void Rebuild() |
|
||||
{ |
|
||||
var length = Text.Length; |
|
||||
|
|
||||
_lines.Clear(); |
|
||||
_rects.Clear(); |
|
||||
_skiaLines = new List<AvaloniaFormattedTextLine>(); |
|
||||
|
|
||||
int curOff = 0; |
|
||||
float curY = 0; |
|
||||
|
|
||||
var metrics = _paint.FontMetrics; |
|
||||
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
|
|
||||
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
|
|
||||
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
|
|
||||
var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
|
|
||||
var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
|
|
||||
var lastLineDescent = mBottom - mDescent; |
|
||||
|
|
||||
// This seems like the best measure of full vertical extent
|
|
||||
// matches Direct2D line height
|
|
||||
_lineHeight = mDescent - mAscent + metrics.Leading; |
|
||||
|
|
||||
// Rendering is relative to baseline
|
|
||||
_lineOffset = (-metrics.Ascent); |
|
||||
|
|
||||
string subString; |
|
||||
|
|
||||
float widthConstraint = double.IsPositiveInfinity(_constraint.Width) |
|
||||
? -1 |
|
||||
: (float)_constraint.Width; |
|
||||
|
|
||||
while(curOff < length) |
|
||||
{ |
|
||||
float lineWidth = -1; |
|
||||
int measured; |
|
||||
int trailingnumber = 0; |
|
||||
|
|
||||
float constraint = -1; |
|
||||
|
|
||||
if (_wrapping == TextWrapping.Wrap) |
|
||||
{ |
|
||||
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; |
|
||||
if (constraint > MAX_LINE_WIDTH) |
|
||||
constraint = MAX_LINE_WIDTH; |
|
||||
} |
|
||||
|
|
||||
measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); |
|
||||
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); |
|
||||
line.Start = curOff; |
|
||||
line.TextLength = measured; |
|
||||
subString = Text.Substring(line.Start, line.TextLength); |
|
||||
lineWidth = MeasureText(line.Start, line.TextLength); |
|
||||
line.Length = measured - trailingnumber; |
|
||||
line.Width = lineWidth; |
|
||||
line.Height = _lineHeight; |
|
||||
line.Top = curY; |
|
||||
|
|
||||
_skiaLines.Add(line); |
|
||||
|
|
||||
curY += _lineHeight; |
|
||||
curY += mLeading; |
|
||||
curOff += measured; |
|
||||
|
|
||||
//if this is the last line and there are trailing newline characters then
|
|
||||
//insert a additional line
|
|
||||
if (curOff >= length) |
|
||||
{ |
|
||||
var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); |
|
||||
var lengthDiff = subString.Length - subStringMinusNewlines.Length; |
|
||||
if (lengthDiff > 0) |
|
||||
{ |
|
||||
AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); |
|
||||
lastLine.TextLength = lengthDiff; |
|
||||
lastLine.Start = curOff - lengthDiff; |
|
||||
var lastLineWidth = MeasureText(line.Start, line.TextLength); |
|
||||
lastLine.Length = 0; |
|
||||
lastLine.Width = lastLineWidth; |
|
||||
lastLine.Height = _lineHeight; |
|
||||
lastLine.Top = curY; |
|
||||
lastLine.IsEmptyTrailingLine = true; |
|
||||
|
|
||||
_skiaLines.Add(lastLine); |
|
||||
|
|
||||
curY += _lineHeight; |
|
||||
curY += mLeading; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Now convert to Avalonia data formats
|
|
||||
_lines.Clear(); |
|
||||
float maxX = 0; |
|
||||
|
|
||||
for (var c = 0; c < _skiaLines.Count; c++) |
|
||||
{ |
|
||||
var w = _skiaLines[c].Width; |
|
||||
if (maxX < w) |
|
||||
maxX = w; |
|
||||
|
|
||||
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height)); |
|
||||
} |
|
||||
|
|
||||
if (_skiaLines.Count == 0) |
|
||||
{ |
|
||||
_lines.Add(new FormattedTextLine(0, _lineHeight)); |
|
||||
_bounds = new Rect(0, 0, 0, _lineHeight); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var lastLine = _skiaLines[_skiaLines.Count - 1]; |
|
||||
_bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height); |
|
||||
|
|
||||
if (double.IsPositiveInfinity(Constraint.Width)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
switch (_paint.TextAlign) |
|
||||
{ |
|
||||
case SKTextAlign.Center: |
|
||||
_bounds = new Rect(Constraint).CenterRect(_bounds); |
|
||||
break; |
|
||||
case SKTextAlign.Right: |
|
||||
_bounds = new Rect( |
|
||||
Constraint.Width - _bounds.Width, |
|
||||
0, |
|
||||
_bounds.Width, |
|
||||
_bounds.Height); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private float MeasureText(int start, int length) |
|
||||
{ |
|
||||
var width = 0f; |
|
||||
|
|
||||
for (int i = start; i < start + length; i++) |
|
||||
{ |
|
||||
var advance = _advances[i]; |
|
||||
|
|
||||
width += advance; |
|
||||
} |
|
||||
|
|
||||
return width; |
|
||||
} |
|
||||
|
|
||||
private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize) |
|
||||
{ |
|
||||
var glyphs = new ushort[text.Length]; |
|
||||
var advances = new float[text.Length]; |
|
||||
|
|
||||
var scale = fontSize / glyphTypeface.DesignEmHeight; |
|
||||
var width = 0f; |
|
||||
var characters = text.AsSpan(); |
|
||||
|
|
||||
for (int i = 0; i < characters.Length; i++) |
|
||||
{ |
|
||||
var c = characters[i]; |
|
||||
float advance; |
|
||||
ushort glyph; |
|
||||
|
|
||||
switch (c) |
|
||||
{ |
|
||||
case (char)0: |
|
||||
{ |
|
||||
glyph = glyphTypeface.GetGlyph(0x200B); |
|
||||
advance = 0; |
|
||||
break; |
|
||||
} |
|
||||
case '\t': |
|
||||
{ |
|
||||
glyph = glyphTypeface.GetGlyph(' '); |
|
||||
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4; |
|
||||
break; |
|
||||
} |
|
||||
default: |
|
||||
{ |
|
||||
glyph = glyphTypeface.GetGlyph(c); |
|
||||
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
glyphs[i] = glyph; |
|
||||
advances[i] = advance; |
|
||||
|
|
||||
width += advance; |
|
||||
} |
|
||||
|
|
||||
_glyphs = new ReadOnlySlice<ushort>(glyphs); |
|
||||
_advances = new ReadOnlySlice<float>(advances); |
|
||||
} |
|
||||
|
|
||||
private float TransformX(float originX, float lineWidth, SKTextAlign align) |
|
||||
{ |
|
||||
float x = 0; |
|
||||
|
|
||||
if (align == SKTextAlign.Left) |
|
||||
{ |
|
||||
x = originX; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ? |
|
||||
Constraint.Width : |
|
||||
_bounds.Width; |
|
||||
|
|
||||
switch (align) |
|
||||
{ |
|
||||
case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break; |
|
||||
case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return x; |
|
||||
} |
|
||||
|
|
||||
private void SetForegroundBrush(IBrush brush, int startIndex, int length) |
|
||||
{ |
|
||||
var key = new FBrushRange(startIndex, length); |
|
||||
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key)); |
|
||||
|
|
||||
if (index > -1) |
|
||||
{ |
|
||||
_foregroundBrushes.RemoveAt(index); |
|
||||
} |
|
||||
|
|
||||
if (brush != null) |
|
||||
{ |
|
||||
brush = brush.ToImmutable(); |
|
||||
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private struct AvaloniaFormattedTextLine |
|
||||
{ |
|
||||
public float Height; |
|
||||
public int Length; |
|
||||
public int Start; |
|
||||
public int TextLength; |
|
||||
public float Top; |
|
||||
public float Width; |
|
||||
public bool IsEmptyTrailingLine; |
|
||||
}; |
|
||||
|
|
||||
private struct FBrushRange |
|
||||
{ |
|
||||
public FBrushRange(int startIndex, int length) |
|
||||
{ |
|
||||
StartIndex = startIndex; |
|
||||
Length = length; |
|
||||
} |
|
||||
|
|
||||
public int EndIndex => StartIndex + Length; |
|
||||
|
|
||||
public int Length { get; private set; } |
|
||||
|
|
||||
public int StartIndex { get; private set; } |
|
||||
|
|
||||
public bool Intersects(int index, int len) => |
|
||||
(index + len) > StartIndex && |
|
||||
(StartIndex + Length) > index; |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
return $"{StartIndex}-{EndIndex}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue