110 changed files with 9179 additions and 1057 deletions
@ -0,0 +1,134 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.TextBlockPage"> |
||||
|
<StackPanel> |
||||
|
<TextBlock Classes="h1">TextBlock</TextBlock> |
||||
|
<TextBlock Classes="h2">A control that can display text</TextBlock> |
||||
|
<StackPanel |
||||
|
Orientation="Horizontal" |
||||
|
Spacing="16" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="0,16,0,0"> |
||||
|
<StackPanel.Styles> |
||||
|
<Style Selector="Border"> |
||||
|
<Setter Property="BorderThickness" Value="1"/> |
||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> |
||||
|
<Setter Property="Padding" Value="2"/> |
||||
|
</Style> |
||||
|
</StackPanel.Styles> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock TextTrimming="CharacterEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/> |
||||
|
<TextBlock TextTrimming="WordEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/> |
||||
|
<TextBlock Text="Left aligned text" TextAlignment="Left" /> |
||||
|
<TextBlock Text="Center aligned text" TextAlignment="Center" /> |
||||
|
<TextBlock Text="Right aligned text" TextAlignment="Right" /> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock |
||||
|
TextWrapping="Wrap" |
||||
|
Text="Multiline TextBlock with TextWrapping.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." /> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/> |
||||
|
<TextBlock Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/> |
||||
|
<TextBlock Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-Italic.ttf#Source Sans Pro"/> |
||||
|
<TextBlock Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
</StackPanel> |
||||
|
<StackPanel |
||||
|
Orientation="Horizontal" |
||||
|
Spacing="16" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="0,16,0,0"> |
||||
|
<StackPanel.Styles> |
||||
|
<Style Selector="Border"> |
||||
|
<Setter Property="BorderThickness" Value="1"/> |
||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> |
||||
|
<Setter Property="Padding" Value="2"/> |
||||
|
</Style> |
||||
|
</StackPanel.Styles> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock TextDecorations="Underline" Text="Underline"/> |
||||
|
<TextBlock TextDecorations="Strikethrough" Text="Strikethrough"/> |
||||
|
<TextBlock TextDecorations="Overline" Text="Overline" /> |
||||
|
<TextBlock TextDecorations="Baseline" Text="Baseline"/> |
||||
|
<TextBlock Text="Custom TextDecorations"> |
||||
|
<TextBlock.TextDecorations> |
||||
|
<TextDecorationCollection> |
||||
|
<TextDecoration |
||||
|
Location="Overline" |
||||
|
PenThicknessUnit="Pixel"> |
||||
|
<TextDecoration.Pen> |
||||
|
<Pen Thickness="2"> |
||||
|
<Pen.Brush> |
||||
|
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%"> |
||||
|
<LinearGradientBrush.GradientStops> |
||||
|
<GradientStop Offset="0" Color="Red"/> |
||||
|
<GradientStop Offset="1" Color="Green"/> |
||||
|
</LinearGradientBrush.GradientStops> |
||||
|
</LinearGradientBrush> |
||||
|
</Pen.Brush> |
||||
|
</Pen> |
||||
|
</TextDecoration.Pen> |
||||
|
</TextDecoration> |
||||
|
<TextDecoration |
||||
|
Location="Strikethrough" |
||||
|
PenThicknessUnit="Pixel"> |
||||
|
<TextDecoration.Pen> |
||||
|
<Pen Thickness="1"> |
||||
|
<Pen.Brush> |
||||
|
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%"> |
||||
|
<LinearGradientBrush.GradientStops> |
||||
|
<GradientStop Offset="0" Color="Green"/> |
||||
|
<GradientStop Offset="1" Color="Blue"/> |
||||
|
</LinearGradientBrush.GradientStops> |
||||
|
</LinearGradientBrush> |
||||
|
</Pen.Brush> |
||||
|
</Pen> |
||||
|
</TextDecoration.Pen> |
||||
|
</TextDecoration> |
||||
|
<TextDecoration |
||||
|
Location="Underline" |
||||
|
PenThicknessUnit="Pixel"> |
||||
|
<TextDecoration.Pen> |
||||
|
<Pen Thickness="2"> |
||||
|
<Pen.Brush> |
||||
|
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%"> |
||||
|
<LinearGradientBrush.GradientStops> |
||||
|
<GradientStop Offset="0" Color="Blue"/> |
||||
|
<GradientStop Offset="1" Color="Red"/> |
||||
|
</LinearGradientBrush.GradientStops> |
||||
|
</LinearGradientBrush> |
||||
|
</Pen.Brush> |
||||
|
</Pen> |
||||
|
</TextDecoration.Pen> |
||||
|
</TextDecoration> |
||||
|
</TextDecorationCollection> |
||||
|
</TextBlock.TextDecorations> |
||||
|
</TextBlock> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock Text="🏻 👌🏻"/> |
||||
|
<TextBlock Text="🏼 👌🏼" /> |
||||
|
<TextBlock Text="🏽 👌🏽"/> |
||||
|
<TextBlock Text="🏾 👌🏾"/> |
||||
|
<TextBlock Text="🏿 👌🏿"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
<Border> |
||||
|
<StackPanel Width="200" Spacing="8"> |
||||
|
<TextBlock Text="👪 👨👩👧 👨👩👧👦"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,18 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public class TextBlockPage : UserControl |
||||
|
{ |
||||
|
public TextBlockPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,56 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media.Immutable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An immutable representation of a <see cref="TextDecoration"/>.
|
||||
|
/// </summary>
|
||||
|
public class ImmutableTextDecoration |
||||
|
{ |
||||
|
public ImmutableTextDecoration(TextDecorationLocation location, ImmutablePen pen, |
||||
|
TextDecorationUnit penThicknessUnit, |
||||
|
double penOffset, TextDecorationUnit penOffsetUnit) |
||||
|
{ |
||||
|
Location = location; |
||||
|
Pen = pen; |
||||
|
PenThicknessUnit = penThicknessUnit; |
||||
|
PenOffset = penOffset; |
||||
|
PenOffsetUnit = penOffsetUnit; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the location.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The location.
|
||||
|
/// </value>
|
||||
|
public TextDecorationLocation Location { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the pen.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The pen.
|
||||
|
/// </value>
|
||||
|
public ImmutablePen Pen { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the units in which the Thickness of the text decoration's <see cref="Pen"/> is expressed.
|
||||
|
/// </summary>
|
||||
|
public TextDecorationUnit PenThicknessUnit { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the pen offset.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The pen offset.
|
||||
|
/// </value>
|
||||
|
public double PenOffset { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the units in which the <see cref="PenOffset"/> value is expressed.
|
||||
|
/// </summary>
|
||||
|
public TextDecorationUnit PenOffsetUnit { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Media.Immutable; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a text decoration, which is a visual ornamentation that is added to text (such as an underline).
|
||||
|
/// </summary>
|
||||
|
public class TextDecoration : AvaloniaObject |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Location"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<TextDecorationLocation> LocationProperty = |
||||
|
AvaloniaProperty.Register<TextDecoration, TextDecorationLocation>(nameof(Location)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Pen"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<IPen> PenProperty = |
||||
|
AvaloniaProperty.Register<TextDecoration, IPen>(nameof(Pen)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="PenThicknessUnit"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<TextDecorationUnit> PenThicknessUnitProperty = |
||||
|
AvaloniaProperty.Register<TextDecoration, TextDecorationUnit>(nameof(PenThicknessUnit)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="PenOffset"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> PenOffsetProperty = |
||||
|
AvaloniaProperty.Register<TextDecoration, double>(nameof(PenOffset)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="PenOffsetUnit"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<TextDecorationUnit> PenOffsetUnitProperty = |
||||
|
AvaloniaProperty.Register<TextDecoration, TextDecorationUnit>(nameof(PenOffsetUnit)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the location.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The location.
|
||||
|
/// </value>
|
||||
|
public TextDecorationLocation Location |
||||
|
{ |
||||
|
get => GetValue(LocationProperty); |
||||
|
set => SetValue(LocationProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the pen.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The pen.
|
||||
|
/// </value>
|
||||
|
public IPen Pen |
||||
|
{ |
||||
|
get => GetValue(PenProperty); |
||||
|
set => SetValue(PenProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the units in which the Thickness of the text decoration's <see cref="Pen"/> is expressed.
|
||||
|
/// </summary>
|
||||
|
public TextDecorationUnit PenThicknessUnit |
||||
|
{ |
||||
|
get => GetValue(PenThicknessUnitProperty); |
||||
|
set => SetValue(PenThicknessUnitProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the pen offset.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The pen offset.
|
||||
|
/// </value>
|
||||
|
public double PenOffset |
||||
|
{ |
||||
|
get => GetValue(PenOffsetProperty); |
||||
|
set => SetValue(PenOffsetProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the units in which the <see cref="PenOffset"/> value is expressed.
|
||||
|
/// </summary>
|
||||
|
public TextDecorationUnit PenOffsetUnit |
||||
|
{ |
||||
|
get => GetValue(PenOffsetUnitProperty); |
||||
|
set => SetValue(PenOffsetUnitProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an immutable clone of the <see cref="TextDecoration"/>.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The immutable clone.</returns>
|
||||
|
public ImmutableTextDecoration ToImmutable() |
||||
|
{ |
||||
|
return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A collection that holds <see cref="TextDecoration"/> objects.
|
||||
|
/// </summary>
|
||||
|
public class TextDecorationCollection : AvaloniaList<TextDecoration> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates an immutable clone of the <see cref="TextDecorationCollection"/>.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The immutable clone.</returns>
|
||||
|
public ImmutableTextDecoration[] ToImmutable() |
||||
|
{ |
||||
|
var immutable = new ImmutableTextDecoration[Count]; |
||||
|
|
||||
|
for (var i = 0; i < Count; i++) |
||||
|
{ |
||||
|
immutable[i] = this[i].ToImmutable(); |
||||
|
} |
||||
|
|
||||
|
return immutable; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a <see cref="TextDecorationCollection"/> string.
|
||||
|
/// </summary>
|
||||
|
/// <param name="s">The string.</param>
|
||||
|
/// <returns>The <see cref="TextDecorationCollection"/>.</returns>
|
||||
|
public static TextDecorationCollection Parse(string s) |
||||
|
{ |
||||
|
var locations = new List<TextDecorationLocation>(); |
||||
|
|
||||
|
using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration.")) |
||||
|
{ |
||||
|
while (tokenizer.TryReadString(out var name)) |
||||
|
{ |
||||
|
var location = GetTextDecorationLocation(name); |
||||
|
|
||||
|
if (locations.Contains(location)) |
||||
|
{ |
||||
|
throw new ArgumentException("Text decoration already specified.", nameof(s)); |
||||
|
} |
||||
|
|
||||
|
locations.Add(location); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var textDecorations = new TextDecorationCollection(); |
||||
|
|
||||
|
foreach (var textDecorationLocation in locations) |
||||
|
{ |
||||
|
textDecorations.Add(new TextDecoration { Location = textDecorationLocation }); |
||||
|
} |
||||
|
|
||||
|
return textDecorations; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a <see cref="TextDecorationLocation"/> string.
|
||||
|
/// </summary>
|
||||
|
/// <param name="s">The string.</param>
|
||||
|
/// <returns>The <see cref="TextDecorationLocation"/>.</returns>
|
||||
|
private static TextDecorationLocation GetTextDecorationLocation(string s) |
||||
|
{ |
||||
|
if (Enum.TryParse<TextDecorationLocation>(s,true, out var location)) |
||||
|
{ |
||||
|
return location; |
||||
|
} |
||||
|
|
||||
|
throw new ArgumentException("Could not parse text decoration.", nameof(s)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Specifies the vertical position of a <see cref="TextDecoration"/> object.
|
||||
|
/// </summary>
|
||||
|
public enum TextDecorationLocation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The underline position.
|
||||
|
/// </summary>
|
||||
|
Underline = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The over line position.
|
||||
|
/// </summary>
|
||||
|
Overline = 1, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The strikethrough position.
|
||||
|
/// </summary>
|
||||
|
Strikethrough = 2, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The baseline position.
|
||||
|
/// </summary>
|
||||
|
Baseline = 3, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Specifies the unit type of either a <see cref="TextDecoration.PenOffset"/> or a <see cref="Pen"/> thickness value.
|
||||
|
/// </summary>
|
||||
|
public enum TextDecorationUnit |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A unit value that is relative to the font used for the <see cref="TextDecoration"/>.
|
||||
|
/// If the decoration spans multiple fonts, an average recommended value is calculated.
|
||||
|
/// This is the default value.
|
||||
|
/// </summary>
|
||||
|
FontRecommended, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A unit value that is relative to the em size of the font.
|
||||
|
/// The value of the offset or thickness is equal to the offset or thickness value multiplied by the font em size.
|
||||
|
/// </summary>
|
||||
|
FontRenderingEmSize, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A unit value that is expressed in pixels.
|
||||
|
/// </summary>
|
||||
|
Pixel |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines a set of commonly used text decorations.
|
||||
|
/// </summary>
|
||||
|
public static class TextDecorations |
||||
|
{ |
||||
|
static TextDecorations() |
||||
|
{ |
||||
|
Underline = new TextDecorationCollection |
||||
|
{ |
||||
|
new TextDecoration |
||||
|
{ |
||||
|
Location = TextDecorationLocation.Underline |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
Strikethrough = new TextDecorationCollection |
||||
|
{ |
||||
|
new TextDecoration |
||||
|
{ |
||||
|
Location = TextDecorationLocation.Strikethrough |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
Overline = new TextDecorationCollection |
||||
|
{ |
||||
|
new TextDecoration |
||||
|
{ |
||||
|
Location = TextDecorationLocation.Overline |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
Baseline = new TextDecorationCollection |
||||
|
{ |
||||
|
new TextDecoration |
||||
|
{ |
||||
|
Location = TextDecorationLocation.Baseline |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="TextDecorationCollection"/> containing an underline.
|
||||
|
/// </summary>
|
||||
|
public static TextDecorationCollection Underline { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="TextDecorationCollection"/> containing a strikethrough.
|
||||
|
/// </summary>
|
||||
|
public static TextDecorationCollection Strikethrough { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="TextDecorationCollection"/> containing an overline.
|
||||
|
/// </summary>
|
||||
|
public static TextDecorationCollection Overline { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="TextDecorationCollection"/> containing a baseline.
|
||||
|
/// </summary>
|
||||
|
public static TextDecorationCollection Baseline { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that supports drawing content.
|
||||
|
/// </summary>
|
||||
|
public abstract class DrawableTextRun : TextRun |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the bounds.
|
||||
|
/// </summary>
|
||||
|
public abstract Rect Bounds { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the <see cref="DrawableTextRun"/> at the given origin.
|
||||
|
/// </summary>
|
||||
|
/// <param name="drawingContext">The drawing context.</param>
|
||||
|
/// <param name="origin">The origin.</param>
|
||||
|
public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A metric that holds information about font specific measurements.
|
||||
|
/// </summary>
|
||||
|
public readonly struct FontMetrics |
||||
|
{ |
||||
|
public FontMetrics(Typeface typeface, double fontSize) |
||||
|
{ |
||||
|
var glyphTypeface = typeface.GlyphTypeface; |
||||
|
|
||||
|
var scale = fontSize / glyphTypeface.DesignEmHeight; |
||||
|
|
||||
|
Ascent = glyphTypeface.Ascent * scale; |
||||
|
|
||||
|
Descent = glyphTypeface.Descent * scale; |
||||
|
|
||||
|
LineGap = glyphTypeface.LineGap * scale; |
||||
|
|
||||
|
LineHeight = Descent - Ascent + LineGap; |
||||
|
|
||||
|
UnderlineThickness = glyphTypeface.UnderlineThickness * scale; |
||||
|
|
||||
|
UnderlinePosition = glyphTypeface.UnderlinePosition * scale; |
||||
|
|
||||
|
StrikethroughThickness = glyphTypeface.StrikethroughThickness * scale; |
||||
|
|
||||
|
StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the recommended distance above the baseline.
|
||||
|
/// </summary>
|
||||
|
public double Ascent { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the recommended distance under the baseline.
|
||||
|
/// </summary>
|
||||
|
public double Descent { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the recommended additional space between two lines of text.
|
||||
|
/// </summary>
|
||||
|
public double LineGap { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the estimated line height.
|
||||
|
/// </summary>
|
||||
|
public double LineHeight { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates the thickness of the underline.
|
||||
|
/// </summary>
|
||||
|
public double UnderlineThickness { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates the distance of the underline from the baseline.
|
||||
|
/// </summary>
|
||||
|
public double UnderlinePosition { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates the thickness of the underline.
|
||||
|
/// </summary>
|
||||
|
public double StrikethroughThickness { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates the distance of the strikethrough from the baseline.
|
||||
|
/// </summary>
|
||||
|
public double StrikethroughPosition { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>.
|
||||
|
/// </summary>
|
||||
|
public interface ITextSource |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets a <see cref="TextRun"/> for specified text source index.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textSourceIndex">The text source index.</param>
|
||||
|
/// <returns>The text run.</returns>
|
||||
|
TextRun GetTextRun(int textSourceIndex); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,218 @@ |
|||||
|
using Avalonia.Media.Immutable; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that holds a shaped glyph run.
|
||||
|
/// </summary>
|
||||
|
public sealed class ShapedTextRun : DrawableTextRun |
||||
|
{ |
||||
|
public ShapedTextRun(ReadOnlySlice<char> text, TextStyle style) : this( |
||||
|
TextShaper.Current.ShapeText(text, style.TextFormat), style) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public ShapedTextRun(GlyphRun glyphRun, TextStyle style) |
||||
|
{ |
||||
|
Text = glyphRun.Characters; |
||||
|
Style = style; |
||||
|
GlyphRun = glyphRun; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bounds.
|
||||
|
/// </summary>
|
||||
|
public override Rect Bounds => GlyphRun.Bounds; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the glyph run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The glyphs.
|
||||
|
/// </value>
|
||||
|
public GlyphRun GlyphRun { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the <see cref="TextRun"/> at the given origin.
|
||||
|
/// </summary>
|
||||
|
/// <param name="drawingContext">The drawing context.</param>
|
||||
|
/// <param name="origin">The origin.</param>
|
||||
|
public override void Draw(IDrawingContextImpl drawingContext, Point origin) |
||||
|
{ |
||||
|
if (GlyphRun.GlyphIndices.Length == 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Style.TextFormat.Typeface == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Style.Foreground == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
drawingContext.DrawGlyphRun(Style.Foreground, GlyphRun, origin); |
||||
|
|
||||
|
if (Style.TextDecorations == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
foreach (var textDecoration in Style.TextDecorations) |
||||
|
{ |
||||
|
DrawTextDecoration(drawingContext, textDecoration, origin); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the <see cref="TextDecoration"/> at given origin.
|
||||
|
/// </summary>
|
||||
|
/// <param name="drawingContext">The drawing context.</param>
|
||||
|
/// <param name="textDecoration">The text decoration.</param>
|
||||
|
/// <param name="origin">The origin.</param>
|
||||
|
private void DrawTextDecoration(IDrawingContextImpl drawingContext, ImmutableTextDecoration textDecoration, Point origin) |
||||
|
{ |
||||
|
var textFormat = Style.TextFormat; |
||||
|
|
||||
|
var fontMetrics = Style.TextFormat.FontMetrics; |
||||
|
|
||||
|
var thickness = textDecoration.Pen?.Thickness ?? 1.0; |
||||
|
|
||||
|
switch (textDecoration.PenThicknessUnit) |
||||
|
{ |
||||
|
case TextDecorationUnit.FontRecommended: |
||||
|
switch (textDecoration.Location) |
||||
|
{ |
||||
|
case TextDecorationLocation.Underline: |
||||
|
thickness = fontMetrics.UnderlineThickness; |
||||
|
break; |
||||
|
case TextDecorationLocation.Strikethrough: |
||||
|
thickness = fontMetrics.StrikethroughThickness; |
||||
|
break; |
||||
|
} |
||||
|
break; |
||||
|
case TextDecorationUnit.FontRenderingEmSize: |
||||
|
thickness = textFormat.FontRenderingEmSize * thickness; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
switch (textDecoration.Location) |
||||
|
{ |
||||
|
case TextDecorationLocation.Overline: |
||||
|
origin += new Point(0, textFormat.FontMetrics.Ascent); |
||||
|
break; |
||||
|
case TextDecorationLocation.Strikethrough: |
||||
|
origin += new Point(0, -textFormat.FontMetrics.StrikethroughPosition); |
||||
|
break; |
||||
|
case TextDecorationLocation.Underline: |
||||
|
origin += new Point(0, -textFormat.FontMetrics.UnderlinePosition); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
switch (textDecoration.PenOffsetUnit) |
||||
|
{ |
||||
|
case TextDecorationUnit.FontRenderingEmSize: |
||||
|
origin += new Point(0, textDecoration.PenOffset * textFormat.FontRenderingEmSize); |
||||
|
break; |
||||
|
case TextDecorationUnit.Pixel: |
||||
|
origin += new Point(0, textDecoration.PenOffset); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
var pen = new ImmutablePen( |
||||
|
textDecoration.Pen?.Brush ?? Style.Foreground.ToImmutable(), |
||||
|
thickness, |
||||
|
textDecoration.Pen?.DashStyle?.ToImmutable(), |
||||
|
textDecoration.Pen?.LineCap ?? default, |
||||
|
textDecoration.Pen?.LineJoin ?? PenLineJoin.Miter, |
||||
|
textDecoration.Pen?.MiterLimit ?? 10.0); |
||||
|
|
||||
|
drawingContext.DrawLine(pen, origin, origin + new Point(GlyphRun.Bounds.Width, 0)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Splits the <see cref="TextRun"/> at specified length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The length.</param>
|
||||
|
/// <returns>The split result.</returns>
|
||||
|
public SplitTextCharactersResult Split(int length) |
||||
|
{ |
||||
|
var glyphCount = 0; |
||||
|
|
||||
|
var firstCharacters = GlyphRun.Characters.Take(length); |
||||
|
|
||||
|
var codepointEnumerator = new CodepointEnumerator(firstCharacters); |
||||
|
|
||||
|
while (codepointEnumerator.MoveNext()) |
||||
|
{ |
||||
|
glyphCount++; |
||||
|
} |
||||
|
|
||||
|
if (GlyphRun.Characters.Length == length) |
||||
|
{ |
||||
|
return new SplitTextCharactersResult(this, null); |
||||
|
} |
||||
|
|
||||
|
if (GlyphRun.GlyphIndices.Length == glyphCount) |
||||
|
{ |
||||
|
return new SplitTextCharactersResult(this, null); |
||||
|
} |
||||
|
|
||||
|
var firstGlyphRun = new GlyphRun( |
||||
|
Style.TextFormat.Typeface.GlyphTypeface, |
||||
|
Style.TextFormat.FontRenderingEmSize, |
||||
|
GlyphRun.GlyphIndices.Take(glyphCount), |
||||
|
GlyphRun.GlyphAdvances.Take(glyphCount), |
||||
|
GlyphRun.GlyphOffsets.Take(glyphCount), |
||||
|
GlyphRun.Characters.Take(length), |
||||
|
GlyphRun.GlyphClusters.Take(length)); |
||||
|
|
||||
|
var firstTextRun = new ShapedTextRun(firstGlyphRun, Style); |
||||
|
|
||||
|
var secondGlyphRun = new GlyphRun( |
||||
|
Style.TextFormat.Typeface.GlyphTypeface, |
||||
|
Style.TextFormat.FontRenderingEmSize, |
||||
|
GlyphRun.GlyphIndices.Skip(glyphCount), |
||||
|
GlyphRun.GlyphAdvances.Skip(glyphCount), |
||||
|
GlyphRun.GlyphOffsets.Skip(glyphCount), |
||||
|
GlyphRun.Characters.Skip(length), |
||||
|
GlyphRun.GlyphClusters.Skip(length)); |
||||
|
|
||||
|
var secondTextRun = new ShapedTextRun(secondGlyphRun, Style); |
||||
|
|
||||
|
return new SplitTextCharactersResult(firstTextRun, secondTextRun); |
||||
|
} |
||||
|
|
||||
|
public readonly struct SplitTextCharactersResult |
||||
|
{ |
||||
|
public SplitTextCharactersResult(ShapedTextRun first, ShapedTextRun second) |
||||
|
{ |
||||
|
First = first; |
||||
|
|
||||
|
Second = second; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the first text run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The first text run.
|
||||
|
/// </value>
|
||||
|
public ShapedTextRun First { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the second text run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The second text run.
|
||||
|
/// </value>
|
||||
|
public ShapedTextRun Second { get; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,446 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal class SimpleTextFormatter : TextFormatter |
||||
|
{ |
||||
|
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Formats a text line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textSource">The text source.</param>
|
||||
|
/// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
|
||||
|
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
|
||||
|
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
|
||||
|
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
|
||||
|
/// <returns>The formatted line.</returns>
|
||||
|
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, |
||||
|
TextParagraphProperties paragraphProperties) |
||||
|
{ |
||||
|
var textTrimming = paragraphProperties.TextTrimming; |
||||
|
var textWrapping = paragraphProperties.TextWrapping; |
||||
|
TextLine textLine; |
||||
|
|
||||
|
var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer); |
||||
|
|
||||
|
if (textTrimming != TextTrimming.None) |
||||
|
{ |
||||
|
textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (textWrapping == TextWrapping.Wrap) |
||||
|
{ |
||||
|
textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var textLineMetrics = |
||||
|
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment); |
||||
|
|
||||
|
textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return textLine; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Formats text runs with optional text style overrides.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textSource">The text source.</param>
|
||||
|
/// <param name="firstTextSourceIndex">The first text source index.</param>
|
||||
|
/// <param name="textPointer">The text pointer that covers the formatted text runs.</param>
|
||||
|
/// <returns>
|
||||
|
/// The formatted text runs.
|
||||
|
/// </returns>
|
||||
|
private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) |
||||
|
{ |
||||
|
var start = firstTextSourceIndex; |
||||
|
|
||||
|
var textRuns = new List<ShapedTextRun>(); |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var textRun = textSource.GetTextRun(firstTextSourceIndex); |
||||
|
|
||||
|
if (textRun.Text.IsEmpty) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (textRun is TextEndOfLine) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (!(textRun is TextCharacters)) |
||||
|
{ |
||||
|
throw new NotSupportedException("Run type not supported by the formatter."); |
||||
|
} |
||||
|
|
||||
|
var runText = textRun.Text; |
||||
|
|
||||
|
while (!runText.IsEmpty) |
||||
|
{ |
||||
|
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); |
||||
|
|
||||
|
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), |
||||
|
shapableTextStyleRun.Style); |
||||
|
|
||||
|
textRuns.Add(shapedRun); |
||||
|
|
||||
|
runText = runText.Skip(shapedRun.Text.Length); |
||||
|
} |
||||
|
|
||||
|
firstTextSourceIndex += textRun.Text.Length; |
||||
|
} |
||||
|
|
||||
|
textPointer = new TextPointer(start, firstTextSourceIndex - start); |
||||
|
|
||||
|
return textRuns; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs text trimming and returns a trimmed line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
|
||||
|
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
|
||||
|
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
|
||||
|
/// <param name="textRuns">The text runs to perform the trimming on.</param>
|
||||
|
/// <param name="text">The text that was used to construct the text runs.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, |
||||
|
double paragraphWidth, TextParagraphProperties paragraphProperties) |
||||
|
{ |
||||
|
var textTrimming = paragraphProperties.TextTrimming; |
||||
|
var availableWidth = paragraphWidth; |
||||
|
var currentWidth = 0.0; |
||||
|
var runIndex = 0; |
||||
|
|
||||
|
while (runIndex < textRuns.Count) |
||||
|
{ |
||||
|
var currentRun = textRuns[runIndex]; |
||||
|
|
||||
|
currentWidth += currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
if (currentWidth > availableWidth) |
||||
|
{ |
||||
|
var ellipsisRun = CreateEllipsisRun(currentRun.Style); |
||||
|
|
||||
|
var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); |
||||
|
|
||||
|
if (textTrimming == TextTrimming.WordEllipsis) |
||||
|
{ |
||||
|
if (measuredLength < text.End) |
||||
|
{ |
||||
|
var currentBreakPosition = 0; |
||||
|
|
||||
|
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
||||
|
|
||||
|
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
||||
|
|
||||
|
if (nextBreakPosition == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (nextBreakPosition > measuredLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
currentBreakPosition = nextBreakPosition; |
||||
|
} |
||||
|
|
||||
|
measuredLength = currentBreakPosition; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (textTrimming == TextTrimming.CharacterEllipsis) |
||||
|
{ |
||||
|
if (measuredLength < text.End) |
||||
|
{ |
||||
|
var currentBreakPosition = 0; |
||||
|
|
||||
|
var graphemeEnumerator = new GraphemeEnumerator(currentRun.Text); |
||||
|
|
||||
|
while (currentBreakPosition < measuredLength && graphemeEnumerator.MoveNext()) |
||||
|
{ |
||||
|
var nextBreakPosition = graphemeEnumerator.Current.Text.End; |
||||
|
|
||||
|
if (nextBreakPosition == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (nextBreakPosition > measuredLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
currentBreakPosition = nextBreakPosition; |
||||
|
} |
||||
|
|
||||
|
measuredLength = currentBreakPosition; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var splitResult = SplitTextRuns(textRuns, measuredLength); |
||||
|
|
||||
|
var trimmedRuns = new List<TextRun>(splitResult.First.Count + 1); |
||||
|
|
||||
|
trimmedRuns.AddRange(splitResult.First); |
||||
|
|
||||
|
trimmedRuns.Add(ellipsisRun); |
||||
|
|
||||
|
var textLineMetrics = |
||||
|
TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); |
||||
|
|
||||
|
return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics); |
||||
|
} |
||||
|
|
||||
|
availableWidth -= currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return new SimpleTextLine(text, textRuns, |
||||
|
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs text wrapping returns a list of text lines.
|
||||
|
/// </summary>
|
||||
|
/// <param name="paragraphProperties">The text paragraph properties.</param>
|
||||
|
/// <param name="textRuns">The text run'S.</param>
|
||||
|
/// <param name="text">The text to analyze for break opportunities.</param>
|
||||
|
/// <param name="paragraphWidth"></param>
|
||||
|
/// <returns></returns>
|
||||
|
private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, |
||||
|
double paragraphWidth, TextParagraphProperties paragraphProperties) |
||||
|
{ |
||||
|
var availableWidth = paragraphWidth; |
||||
|
var currentWidth = 0.0; |
||||
|
var runIndex = 0; |
||||
|
|
||||
|
while (runIndex < textRuns.Count) |
||||
|
{ |
||||
|
var currentRun = textRuns[runIndex]; |
||||
|
|
||||
|
currentWidth += currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
if (currentWidth > availableWidth) |
||||
|
{ |
||||
|
var measuredLength = MeasureText(currentRun, paragraphWidth); |
||||
|
|
||||
|
if (measuredLength < text.End) |
||||
|
{ |
||||
|
var currentBreakPosition = -1; |
||||
|
|
||||
|
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
||||
|
|
||||
|
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
||||
|
|
||||
|
if (nextBreakPosition == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (nextBreakPosition > measuredLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
currentBreakPosition = nextBreakPosition; |
||||
|
} |
||||
|
|
||||
|
if (currentBreakPosition != -1) |
||||
|
{ |
||||
|
measuredLength = currentBreakPosition; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var splitResult = SplitTextRuns(textRuns, measuredLength); |
||||
|
|
||||
|
var textLineMetrics = |
||||
|
TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); |
||||
|
|
||||
|
return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics); |
||||
|
} |
||||
|
|
||||
|
availableWidth -= currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return new SimpleTextLine(text, textRuns, |
||||
|
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures the number of characters that fits into available width.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRun">The text run.</param>
|
||||
|
/// <param name="availableWidth">The available width.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private int MeasureText(ShapedTextRun textRun, double availableWidth) |
||||
|
{ |
||||
|
if (textRun.GlyphRun.Bounds.Width < availableWidth) |
||||
|
{ |
||||
|
return textRun.Text.Length; |
||||
|
} |
||||
|
|
||||
|
var measuredWidth = 0.0; |
||||
|
|
||||
|
var index = 0; |
||||
|
|
||||
|
for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++) |
||||
|
{ |
||||
|
var advance = textRun.GlyphRun.GlyphAdvances[index]; |
||||
|
|
||||
|
if (measuredWidth + advance > availableWidth) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
measuredWidth += advance; |
||||
|
} |
||||
|
|
||||
|
var cluster = textRun.GlyphRun.GlyphClusters[index]; |
||||
|
|
||||
|
var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _); |
||||
|
|
||||
|
return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start + |
||||
|
(textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an ellipsis.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textStyle">The text style.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle) |
||||
|
{ |
||||
|
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>(); |
||||
|
|
||||
|
var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat); |
||||
|
|
||||
|
return new ShapedTextRun(glyphRun, textStyle); |
||||
|
} |
||||
|
|
||||
|
private readonly struct SplitTextRunsResult |
||||
|
{ |
||||
|
public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second) |
||||
|
{ |
||||
|
First = first; |
||||
|
|
||||
|
Second = second; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the first text runs.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The first text runs.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<ShapedTextRun> First { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the second text runs.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The second text runs.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<ShapedTextRun> Second { get; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Split a sequence of runs into two segments at specified length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text run's.</param>
|
||||
|
/// <param name="length">The length to split at.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length) |
||||
|
{ |
||||
|
var currentLength = 0; |
||||
|
|
||||
|
for (var i = 0; i < textRuns.Count; i++) |
||||
|
{ |
||||
|
var currentRun = textRuns[i]; |
||||
|
|
||||
|
if (currentLength + currentRun.GlyphRun.Characters.Length < length) |
||||
|
{ |
||||
|
currentLength += currentRun.GlyphRun.Characters.Length; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i; |
||||
|
|
||||
|
var first = new ShapedTextRun[firstCount]; |
||||
|
|
||||
|
if (firstCount > 1) |
||||
|
{ |
||||
|
for (var j = 0; j < i; j++) |
||||
|
{ |
||||
|
first[j] = textRuns[j]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var secondCount = textRuns.Count - firstCount; |
||||
|
|
||||
|
if (currentLength + currentRun.GlyphRun.Characters.Length == length) |
||||
|
{ |
||||
|
var second = new ShapedTextRun[secondCount]; |
||||
|
|
||||
|
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; |
||||
|
|
||||
|
if (secondCount > 0) |
||||
|
{ |
||||
|
for (var j = 0; j < secondCount; j++) |
||||
|
{ |
||||
|
second[j] = textRuns[i + j + offset]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
first[i] = currentRun; |
||||
|
|
||||
|
return new SplitTextRunsResult(first, second); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
secondCount++; |
||||
|
|
||||
|
var second = new ShapedTextRun[secondCount]; |
||||
|
|
||||
|
if (secondCount > 0) |
||||
|
{ |
||||
|
for (var j = 1; j < secondCount; j++) |
||||
|
{ |
||||
|
second[j] = textRuns[i + j]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var split = currentRun.Split(length - currentLength); |
||||
|
|
||||
|
first[i] = split.First; |
||||
|
|
||||
|
second[0] = split.Second; |
||||
|
|
||||
|
return new SplitTextRunsResult(first, second); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new SplitTextRunsResult(textRuns, null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,283 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal class SimpleTextLine : TextLine |
||||
|
{ |
||||
|
public SimpleTextLine(TextPointer textPointer, IReadOnlyList<TextRun> textRuns, TextLineMetrics lineMetrics) : |
||||
|
base(textPointer, textRuns, lineMetrics) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public override void Draw(IDrawingContextImpl drawingContext, Point origin) |
||||
|
{ |
||||
|
var currentX = origin.X; |
||||
|
|
||||
|
foreach (var textRun in TextRuns) |
||||
|
{ |
||||
|
if (!(textRun is DrawableTextRun drawableRun)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, |
||||
|
origin.Y + LineMetrics.BaselineOrigin.Y); |
||||
|
|
||||
|
drawableRun.Draw(drawingContext, baselineOrigin); |
||||
|
|
||||
|
currentX += drawableRun.Bounds.Width; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the character hit corresponding to the specified
|
||||
|
/// distance from the beginning of the line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="distance">distance in text flow direction from the beginning of the line</param>
|
||||
|
/// <returns>character hit</returns>
|
||||
|
public override CharacterHit GetCharacterHitFromDistance(double distance) |
||||
|
{ |
||||
|
var first = Text.Start; |
||||
|
|
||||
|
if (distance < 0) |
||||
|
{ |
||||
|
// hit happens before the line, return the first position
|
||||
|
return new CharacterHit(Text.Start); |
||||
|
} |
||||
|
|
||||
|
// process hit that happens within the line
|
||||
|
var runIndex = new CharacterHit(); |
||||
|
|
||||
|
foreach (var run in TextRuns) |
||||
|
{ |
||||
|
var shapedTextRun = (ShapedTextRun)run; |
||||
|
|
||||
|
first += runIndex.TrailingLength; |
||||
|
|
||||
|
runIndex = shapedTextRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); |
||||
|
|
||||
|
first += runIndex.FirstCharacterIndex; |
||||
|
|
||||
|
if (distance <= shapedTextRun.Bounds.Width) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
distance -= shapedTextRun.Bounds.Width; |
||||
|
} |
||||
|
|
||||
|
return new CharacterHit(first, runIndex.TrailingLength); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the distance from the beginning of the line from the specified
|
||||
|
/// character hit.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">character hit of the character to query the distance.</param>
|
||||
|
/// <returns>distance in text flow direction from the beginning of the line.</returns>
|
||||
|
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
return DistanceFromCp(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the next character hit for caret navigation
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">the current character hit</param>
|
||||
|
/// <returns>the next character hit</returns>
|
||||
|
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
int nextVisibleCp; |
||||
|
bool navigableCpFound; |
||||
|
|
||||
|
if (characterHit.TrailingLength == 0) |
||||
|
{ |
||||
|
navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex, out nextVisibleCp); |
||||
|
|
||||
|
if (navigableCpFound) |
||||
|
{ |
||||
|
// Move from leading to trailing edge
|
||||
|
return new CharacterHit(nextVisibleCp, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); |
||||
|
|
||||
|
if (navigableCpFound) |
||||
|
{ |
||||
|
// Move from trailing edge of current character to trailing edge of next
|
||||
|
return new CharacterHit(nextVisibleCp, 1); |
||||
|
} |
||||
|
|
||||
|
// Can't move, we're after the last character
|
||||
|
return characterHit; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the previous character hit for caret navigation
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">the current character hit</param>
|
||||
|
/// <returns>the previous character hit</returns>
|
||||
|
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
int previousVisibleCp; |
||||
|
bool navigableCpFound; |
||||
|
|
||||
|
int cpHit = characterHit.FirstCharacterIndex; |
||||
|
bool trailingHit = (characterHit.TrailingLength != 0); |
||||
|
|
||||
|
// Input can be right after the end of the current line. Snap it to be at the end of the line.
|
||||
|
if (cpHit >= Text.Start + Text.Length) |
||||
|
{ |
||||
|
cpHit = Text.Start + Text.Length - 1; |
||||
|
|
||||
|
trailingHit = true; |
||||
|
} |
||||
|
|
||||
|
if (trailingHit) |
||||
|
{ |
||||
|
navigableCpFound = FindPreviousVisibleCp(cpHit, out previousVisibleCp); |
||||
|
|
||||
|
if (navigableCpFound) |
||||
|
{ |
||||
|
// Move from trailing to leading edge
|
||||
|
return new CharacterHit(previousVisibleCp, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigableCpFound = FindPreviousVisibleCp(cpHit - 1, out previousVisibleCp); |
||||
|
|
||||
|
if (navigableCpFound) |
||||
|
{ |
||||
|
// Move from leading edge of current character to leading edge of previous
|
||||
|
return new CharacterHit(previousVisibleCp, 0); |
||||
|
} |
||||
|
|
||||
|
// Can't move, we're before the first character
|
||||
|
return characterHit; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the previous character hit after backspacing
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">the current character hit</param>
|
||||
|
/// <returns>the character hit after backspacing</returns>
|
||||
|
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
// same operation as move-to-previous
|
||||
|
return GetPreviousCaretCharacterHit(characterHit); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get distance from line start to the specified cp
|
||||
|
/// </summary>
|
||||
|
private double DistanceFromCp(int currentIndex) |
||||
|
{ |
||||
|
var distance = 0.0; |
||||
|
var dcp = currentIndex - Text.Start; |
||||
|
|
||||
|
foreach (var textRun in TextRuns) |
||||
|
{ |
||||
|
var run = (ShapedTextRun)textRun; |
||||
|
|
||||
|
distance += run.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(dcp)); |
||||
|
|
||||
|
if (dcp <= run.Text.Length) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
dcp -= run.Text.Length; |
||||
|
} |
||||
|
|
||||
|
return distance; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Search forward from the given cp index (inclusive) to find the next navigable cp index.
|
||||
|
/// Return true if one such cp is found, false otherwise.
|
||||
|
/// </summary>
|
||||
|
private bool FindNextVisibleCp(int cp, out int cpVisible) |
||||
|
{ |
||||
|
cpVisible = cp; |
||||
|
|
||||
|
if (cp >= Text.Start + Text.Length) |
||||
|
{ |
||||
|
return false; // Cannot go forward anymore
|
||||
|
} |
||||
|
|
||||
|
GetRunIndexAtCp(cp, out var runIndex, out var cpRunStart); |
||||
|
|
||||
|
while (runIndex < TextRuns.Count) |
||||
|
{ |
||||
|
// When navigating forward, only the trailing edge of visible content is
|
||||
|
// navigable.
|
||||
|
if (runIndex < TextRuns.Count) |
||||
|
{ |
||||
|
cpVisible = Math.Max(cpRunStart, cp); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
cpRunStart += TextRuns[runIndex++].Text.Length; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Search backward from the given cp index (inclusive) to find the previous navigable cp index.
|
||||
|
/// Return true if one such cp is found, false otherwise.
|
||||
|
/// </summary>
|
||||
|
private bool FindPreviousVisibleCp(int cp, out int cpVisible) |
||||
|
{ |
||||
|
cpVisible = cp; |
||||
|
|
||||
|
if (cp < Text.Start) |
||||
|
{ |
||||
|
return false; // Cannot go backward anymore.
|
||||
|
} |
||||
|
|
||||
|
// Position the cpRunEnd at the end of the span that contains the given cp
|
||||
|
GetRunIndexAtCp(cp, out var runIndex, out var cpRunEnd); |
||||
|
|
||||
|
cpRunEnd += TextRuns[runIndex].Text.End; |
||||
|
|
||||
|
while (runIndex >= 0) |
||||
|
{ |
||||
|
// Visible content has caret stops at its leading edge.
|
||||
|
if (runIndex + 1 < TextRuns.Count) |
||||
|
{ |
||||
|
cpVisible = Math.Min(cpRunEnd, cp); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Newline sequence has caret stops at its leading edge.
|
||||
|
if (runIndex == TextRuns.Count) |
||||
|
{ |
||||
|
// Get the cp index at the beginning of the newline sequence.
|
||||
|
cpVisible = cpRunEnd - TextRuns[runIndex].Text.Length + 1; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
cpRunEnd -= TextRuns[runIndex--].Text.Length; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private void GetRunIndexAtCp(int cp, out int runIndex, out int cpRunStart) |
||||
|
{ |
||||
|
cpRunStart = Text.Start; |
||||
|
runIndex = 0; |
||||
|
|
||||
|
// Find the span that contains the given cp
|
||||
|
while (runIndex < TextRuns.Count && cpRunStart + TextRuns[runIndex].Text.Length <= cp) |
||||
|
{ |
||||
|
cpRunStart += TextRuns[runIndex++].Text.Length; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that holds text characters.
|
||||
|
/// </summary>
|
||||
|
public class TextCharacters : TextRun |
||||
|
{ |
||||
|
protected TextCharacters() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public TextCharacters(ReadOnlySlice<char> text, TextStyle style) |
||||
|
{ |
||||
|
Text = text; |
||||
|
Style = style; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that indicates the end of a line.
|
||||
|
/// </summary>
|
||||
|
public class TextEndOfLine : TextRun |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that indicates the end of a paragraph.
|
||||
|
/// </summary>
|
||||
|
public class TextEndOfParagraph : TextEndOfLine |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Unique text formatting properties that are used by the <see cref="TextFormatter"/>.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextFormat : IEquatable<TextFormat> |
||||
|
{ |
||||
|
public TextFormat(Typeface typeface, double fontRenderingEmSize) |
||||
|
{ |
||||
|
Typeface = typeface; |
||||
|
FontRenderingEmSize = fontRenderingEmSize; |
||||
|
FontMetrics = new FontMetrics(typeface, fontRenderingEmSize); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the typeface.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The typeface.
|
||||
|
/// </value>
|
||||
|
public Typeface Typeface { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the font rendering em size.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The em rendering size of the font.
|
||||
|
/// </value>
|
||||
|
public double FontRenderingEmSize { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the font metrics.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The metrics of the font.
|
||||
|
/// </value>
|
||||
|
public FontMetrics FontMetrics { get; } |
||||
|
|
||||
|
public static bool operator ==(TextFormat self, TextFormat other) |
||||
|
{ |
||||
|
return self.Equals(other); |
||||
|
} |
||||
|
|
||||
|
public static bool operator !=(TextFormat self, TextFormat other) |
||||
|
{ |
||||
|
return !(self == other); |
||||
|
} |
||||
|
|
||||
|
public bool Equals(TextFormat other) |
||||
|
{ |
||||
|
return Typeface.Equals(other.Typeface) && FontRenderingEmSize.Equals(other.FontRenderingEmSize); |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
return obj is TextFormat other && Equals(other); |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
unchecked |
||||
|
{ |
||||
|
var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); |
||||
|
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); |
||||
|
return hashCode; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,186 @@ |
|||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a base class for text formatting.
|
||||
|
/// </summary>
|
||||
|
public abstract class TextFormatter |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the current <see cref="TextFormatter"/> that is used for non complex text formatting.
|
||||
|
/// </summary>
|
||||
|
public static TextFormatter Current |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
var current = AvaloniaLocator.Current.GetService<TextFormatter>(); |
||||
|
|
||||
|
if (current != null) |
||||
|
{ |
||||
|
return current; |
||||
|
} |
||||
|
|
||||
|
current = new SimpleTextFormatter(); |
||||
|
|
||||
|
AvaloniaLocator.CurrentMutable.Bind<TextFormatter>().ToConstant(current); |
||||
|
|
||||
|
return current; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Formats a text line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textSource">The text source.</param>
|
||||
|
/// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
|
||||
|
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
|
||||
|
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
|
||||
|
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
|
||||
|
/// <returns>The formatted line.</returns>
|
||||
|
public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, |
||||
|
TextParagraphProperties paragraphProperties); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a text style run with unique properties.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The text to create text runs from.</param>
|
||||
|
/// <param name="defaultStyle"></param>
|
||||
|
/// <returns>A list of text runs.</returns>
|
||||
|
protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice<char> text, TextStyle defaultStyle) |
||||
|
{ |
||||
|
var defaultTypeface = defaultStyle.TextFormat.Typeface; |
||||
|
|
||||
|
var currentTypeface = defaultTypeface; |
||||
|
|
||||
|
if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) |
||||
|
{ |
||||
|
return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, |
||||
|
defaultStyle.TextFormat.FontRenderingEmSize, |
||||
|
defaultStyle.Foreground, defaultStyle.TextDecorations)); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
var codepoint = Codepoint.ReadAt(text, count, out _); |
||||
|
|
||||
|
//ToDo: Fix FontFamily fallback
|
||||
|
currentTypeface = |
||||
|
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); |
||||
|
|
||||
|
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) |
||||
|
{ |
||||
|
//Fallback found
|
||||
|
return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, |
||||
|
defaultStyle.TextFormat.FontRenderingEmSize, |
||||
|
defaultStyle.Foreground, defaultStyle.TextDecorations)); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// no fallback found
|
||||
|
currentTypeface = defaultTypeface; |
||||
|
|
||||
|
var glyphTypeface = currentTypeface.GlyphTypeface; |
||||
|
|
||||
|
var enumerator = new GraphemeEnumerator(text); |
||||
|
|
||||
|
while (enumerator.MoveNext()) |
||||
|
{ |
||||
|
var grapheme = enumerator.Current; |
||||
|
|
||||
|
if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
count += grapheme.Text.Length; |
||||
|
} |
||||
|
|
||||
|
return new TextStyleRun(new TextPointer(text.Start, count), |
||||
|
new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, |
||||
|
defaultStyle.Foreground, defaultStyle.TextDecorations)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to get run properties.
|
||||
|
/// </summary>
|
||||
|
/// <param name="defaultTypeface"></param>
|
||||
|
/// <param name="text"></param>
|
||||
|
/// <param name="typeface">The typeface that is used to find matching characters.</param>
|
||||
|
/// <param name="count"></param>
|
||||
|
/// <returns></returns>
|
||||
|
protected bool TryGetRunProperties(ReadOnlySlice<char> text, Typeface typeface, Typeface defaultTypeface, |
||||
|
out int count) |
||||
|
{ |
||||
|
if (text.Length == 0) |
||||
|
{ |
||||
|
count = 0; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var isFallback = typeface != defaultTypeface; |
||||
|
|
||||
|
count = 0; |
||||
|
var script = Script.Common; |
||||
|
//var direction = BiDiClass.LeftToRight;
|
||||
|
|
||||
|
var font = typeface.GlyphTypeface; |
||||
|
var defaultFont = defaultTypeface.GlyphTypeface; |
||||
|
|
||||
|
var enumerator = new GraphemeEnumerator(text); |
||||
|
|
||||
|
while (enumerator.MoveNext()) |
||||
|
{ |
||||
|
var grapheme = enumerator.Current; |
||||
|
|
||||
|
var currentScript = grapheme.FirstCodepoint.Script; |
||||
|
|
||||
|
//var currentDirection = grapheme.FirstCodepoint.BiDiClass;
|
||||
|
|
||||
|
//// ToDo: Implement BiDi algorithm
|
||||
|
//if (currentScript.HorizontalDirection != direction)
|
||||
|
//{
|
||||
|
// if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
|
||||
|
// {
|
||||
|
// break;
|
||||
|
// }
|
||||
|
//}
|
||||
|
|
||||
|
if (currentScript != script) |
||||
|
{ |
||||
|
if (currentScript != Script.Inherited && currentScript != Script.Common) |
||||
|
{ |
||||
|
if (script == Script.Inherited || script == Script.Common) |
||||
|
{ |
||||
|
script = currentScript; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isFallback) |
||||
|
{ |
||||
|
if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) |
||||
|
{ |
||||
|
if (!grapheme.FirstCodepoint.IsWhiteSpace) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
count += grapheme.Text.Length; |
||||
|
} |
||||
|
|
||||
|
return count > 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,382 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a multi line text layout.
|
||||
|
/// </summary>
|
||||
|
public class TextLayout |
||||
|
{ |
||||
|
private static readonly ReadOnlySlice<char> s_empty = new ReadOnlySlice<char>(new[] { '\u200B' }); |
||||
|
|
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly TextParagraphProperties _paragraphProperties; |
||||
|
private readonly TextStyleRun[] _textStyleOverrides; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="TextLayout" /> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The text.</param>
|
||||
|
/// <param name="typeface">The typeface.</param>
|
||||
|
/// <param name="fontSize">Size of the font.</param>
|
||||
|
/// <param name="foreground">The foreground.</param>
|
||||
|
/// <param name="textAlignment">The text alignment.</param>
|
||||
|
/// <param name="textWrapping">The text wrapping.</param>
|
||||
|
/// <param name="textTrimming">The text trimming.</param>
|
||||
|
/// <param name="textDecorations">The text decorations.</param>
|
||||
|
/// <param name="maxWidth">The maximum width.</param>
|
||||
|
/// <param name="maxHeight">The maximum height.</param>
|
||||
|
/// <param name="textStyleOverrides">The text style overrides.</param>
|
||||
|
public TextLayout( |
||||
|
string text, |
||||
|
Typeface typeface, |
||||
|
double fontSize, |
||||
|
IBrush foreground, |
||||
|
TextAlignment textAlignment = TextAlignment.Left, |
||||
|
TextWrapping textWrapping = TextWrapping.NoWrap, |
||||
|
TextTrimming textTrimming = TextTrimming.None, |
||||
|
TextDecorationCollection textDecorations = null, |
||||
|
double maxWidth = double.PositiveInfinity, |
||||
|
double maxHeight = double.PositiveInfinity, |
||||
|
TextStyleRun[] textStyleOverrides = null) |
||||
|
{ |
||||
|
_text = string.IsNullOrEmpty(text) ? |
||||
|
new ReadOnlySlice<char>() : |
||||
|
new ReadOnlySlice<char>(text.AsMemory()); |
||||
|
|
||||
|
_paragraphProperties = |
||||
|
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable()); |
||||
|
|
||||
|
_textStyleOverrides = textStyleOverrides; |
||||
|
|
||||
|
MaxWidth = maxWidth; |
||||
|
|
||||
|
MaxHeight = maxHeight; |
||||
|
|
||||
|
UpdateLayout(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the maximum width.
|
||||
|
/// </summary>
|
||||
|
public double MaxWidth { get; } |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the maximum height.
|
||||
|
/// </summary>
|
||||
|
public double MaxHeight { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text lines.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The text lines.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<TextLine> TextLines { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bounds of the layout.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The bounds.
|
||||
|
/// </value>
|
||||
|
public Rect Bounds { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the text layout.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">The drawing context.</param>
|
||||
|
/// <param name="origin">The origin.</param>
|
||||
|
public void Draw(IDrawingContextImpl context, Point origin) |
||||
|
{ |
||||
|
if (!TextLines.Any()) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var currentY = origin.Y; |
||||
|
|
||||
|
foreach (var textLine in TextLines) |
||||
|
{ |
||||
|
textLine.Draw(context, new Point(origin.X, currentY)); |
||||
|
|
||||
|
currentY += textLine.LineMetrics.Size.Height; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the default <see cref="TextParagraphProperties"/> that are used by the <see cref="TextFormatter"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="typeface">The typeface.</param>
|
||||
|
/// <param name="fontSize">The font size.</param>
|
||||
|
/// <param name="foreground">The foreground.</param>
|
||||
|
/// <param name="textAlignment">The text alignment.</param>
|
||||
|
/// <param name="textWrapping">The text wrapping.</param>
|
||||
|
/// <param name="textTrimming">The text trimming.</param>
|
||||
|
/// <param name="textDecorations">The text decorations.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, |
||||
|
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, |
||||
|
ImmutableTextDecoration[] textDecorations) |
||||
|
{ |
||||
|
var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations); |
||||
|
|
||||
|
return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the current bounds.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textLine">The text line.</param>
|
||||
|
/// <param name="left">The left.</param>
|
||||
|
/// <param name="right">The right.</param>
|
||||
|
/// <param name="bottom">The bottom.</param>
|
||||
|
private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) |
||||
|
{ |
||||
|
if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) |
||||
|
{ |
||||
|
right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; |
||||
|
} |
||||
|
|
||||
|
if (left < textLine.LineMetrics.BaselineOrigin.X) |
||||
|
{ |
||||
|
left = textLine.LineMetrics.BaselineOrigin.X; |
||||
|
} |
||||
|
|
||||
|
bottom += textLine.LineMetrics.Size.Height; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an empty text line.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The empty text line.</returns>
|
||||
|
private TextLine CreateEmptyTextLine(int startingIndex) |
||||
|
{ |
||||
|
var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat; |
||||
|
|
||||
|
var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat); |
||||
|
|
||||
|
var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) }; |
||||
|
|
||||
|
return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns, |
||||
|
TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the layout and applies specified text style overrides.
|
||||
|
/// </summary>
|
||||
|
private void UpdateLayout() |
||||
|
{ |
||||
|
if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) |
||||
|
{ |
||||
|
var textLine = CreateEmptyTextLine(0); |
||||
|
|
||||
|
TextLines = new List<TextLine> { textLine }; |
||||
|
|
||||
|
Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var textLines = new List<TextLine>(); |
||||
|
|
||||
|
double left = 0.0, right = 0.0, bottom = 0.0; |
||||
|
|
||||
|
var lineBreaker = new LineBreakEnumerator(_text); |
||||
|
|
||||
|
var currentPosition = 0; |
||||
|
|
||||
|
while (currentPosition < _text.Length) |
||||
|
{ |
||||
|
int length; |
||||
|
|
||||
|
if (lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
if (!lineBreaker.Current.Required) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
length = lineBreaker.Current.PositionWrap - currentPosition; |
||||
|
|
||||
|
if (currentPosition + length < _text.Length) |
||||
|
{ |
||||
|
//The line breaker isn't treating \n\r as a pair so we have to fix that here.
|
||||
|
if (_text[lineBreaker.Current.PositionMeasure] == '\n' |
||||
|
&& _text[lineBreaker.Current.PositionWrap] == '\r') |
||||
|
{ |
||||
|
length++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
length = _text.Length - currentPosition; |
||||
|
} |
||||
|
|
||||
|
var remainingLength = length; |
||||
|
|
||||
|
while (remainingLength > 0) |
||||
|
{ |
||||
|
var textSlice = _text.AsSlice(currentPosition, remainingLength); |
||||
|
|
||||
|
var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides); |
||||
|
|
||||
|
var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); |
||||
|
|
||||
|
UpdateBounds(textLine, ref left, ref right, ref bottom); |
||||
|
|
||||
|
textLines.Add(textLine); |
||||
|
|
||||
|
if (_paragraphProperties.TextTrimming != TextTrimming.None) |
||||
|
{ |
||||
|
currentPosition += remainingLength; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
remainingLength -= textLine.Text.Length; |
||||
|
|
||||
|
currentPosition += textLine.Text.Length; |
||||
|
} |
||||
|
|
||||
|
if (lineBreaker.Current.Required && currentPosition == _text.Length) |
||||
|
{ |
||||
|
var emptyTextLine = CreateEmptyTextLine(currentPosition); |
||||
|
|
||||
|
UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); |
||||
|
|
||||
|
textLines.Add(emptyTextLine); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Bounds = new Rect(left, 0, right, bottom); |
||||
|
|
||||
|
TextLines = textLines; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private struct FormattedTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly TextStyle _defaultStyle; |
||||
|
private readonly TextStyleRun[] _textStyleOverrides; |
||||
|
|
||||
|
public FormattedTextSource(ReadOnlySlice<char> text, TextStyle defaultStyle, |
||||
|
TextStyleRun[] textStyleOverrides) |
||||
|
{ |
||||
|
_text = text; |
||||
|
_defaultStyle = defaultStyle; |
||||
|
_textStyleOverrides = textStyleOverrides; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
var runText = _text.Skip(textSourceIndex); |
||||
|
|
||||
|
if (runText.IsEmpty) |
||||
|
{ |
||||
|
return new TextEndOfLine(); |
||||
|
} |
||||
|
|
||||
|
var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides); |
||||
|
|
||||
|
return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a text style run that has overrides applied. Only overrides with equal TextStyle.
|
||||
|
/// If optimizeForShaping is <c>true</c> Foreground is ignored.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The text to create the run for.</param>
|
||||
|
/// <param name="defaultTextStyle">The default text style for segments that don't have an override.</param>
|
||||
|
/// <param name="textStyleOverrides">The text style overrides.</param>
|
||||
|
/// <returns>
|
||||
|
/// The created text style run.
|
||||
|
/// </returns>
|
||||
|
private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice<char> text, |
||||
|
TextStyle defaultTextStyle, ReadOnlySpan<TextStyleRun> textStyleOverrides) |
||||
|
{ |
||||
|
var currentTextStyle = defaultTextStyle; |
||||
|
|
||||
|
var hasOverride = false; |
||||
|
|
||||
|
var i = 0; |
||||
|
|
||||
|
var length = 0; |
||||
|
|
||||
|
for (; i < textStyleOverrides.Length; i++) |
||||
|
{ |
||||
|
var styleOverride = textStyleOverrides[i]; |
||||
|
|
||||
|
var textPointer = styleOverride.TextPointer; |
||||
|
|
||||
|
if (textPointer.End < text.Start) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (textPointer.Start > text.End) |
||||
|
{ |
||||
|
length = text.Length; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (textPointer.Start > text.Start) |
||||
|
{ |
||||
|
if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat || |
||||
|
currentTextStyle.Foreground != styleOverride.Style.Foreground) |
||||
|
{ |
||||
|
length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
length += Math.Min(text.Length - length, textPointer.Length); |
||||
|
|
||||
|
if (hasOverride) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
hasOverride = true; |
||||
|
|
||||
|
currentTextStyle = styleOverride.Style; |
||||
|
} |
||||
|
|
||||
|
if (length < text.Length && i == textStyleOverrides.Length) |
||||
|
{ |
||||
|
if (currentTextStyle.Foreground == defaultTextStyle.Foreground && |
||||
|
currentTextStyle.TextFormat == defaultTextStyle.TextFormat) |
||||
|
{ |
||||
|
length = text.Length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (length != text.Length) |
||||
|
{ |
||||
|
text = text.Take(length); |
||||
|
} |
||||
|
|
||||
|
return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a line of text that is used for text rendering.
|
||||
|
/// </summary>
|
||||
|
public abstract class TextLine |
||||
|
{ |
||||
|
protected TextLine() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected TextLine(TextPointer text, IReadOnlyList<TextRun> textRuns, TextLineMetrics lineMetrics) |
||||
|
{ |
||||
|
Text = text; |
||||
|
TextRuns = textRuns; |
||||
|
LineMetrics = lineMetrics; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The text pointer.
|
||||
|
/// </value>
|
||||
|
public TextPointer Text { get; protected set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text runs.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The text runs.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<TextRun> TextRuns { get; protected set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the line metrics.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The line metrics.
|
||||
|
/// </value>
|
||||
|
public TextLineMetrics LineMetrics { get; protected set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the <see cref="TextLine"/> at the given origin.
|
||||
|
/// </summary>
|
||||
|
/// <param name="drawingContext">The drawing context.</param>
|
||||
|
/// <param name="origin">The origin.</param>
|
||||
|
public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the character hit corresponding to the specified
|
||||
|
/// distance from the beginning of the line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="distance">distance in text flow direction from the beginning of the line</param>
|
||||
|
/// <returns>The <see cref="CharacterHit"/></returns>
|
||||
|
public abstract CharacterHit GetCharacterHitFromDistance(double distance); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the distance from the beginning of the line from the specified
|
||||
|
/// <see cref="CharacterHit"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit"><see cref="CharacterHit"/> of the character to query the distance.</param>
|
||||
|
/// <returns>Distance in text flow direction from the beginning of the line.</returns>
|
||||
|
public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the next <see cref="CharacterHit"/> for caret navigation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">The current <see cref="CharacterHit"/>.</param>
|
||||
|
/// <returns>The next <see cref="CharacterHit"/>.</returns>
|
||||
|
public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the previous character hit for caret navigation
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">the current character hit</param>
|
||||
|
/// <returns>The previous <see cref="CharacterHit"/></returns>
|
||||
|
public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Client to get the previous character hit after backspacing
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">the current character hit</param>
|
||||
|
/// <returns>The <see cref="CharacterHit"/> after backspacing</returns>
|
||||
|
public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text line offset x.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lineWidth">The line width.</param>
|
||||
|
/// <param name="paragraphWidth">The paragraph width.</param>
|
||||
|
/// <param name="textAlignment">The text alignment.</param>
|
||||
|
/// <returns>The paragraph offset.</returns>
|
||||
|
internal static double GetParagraphOffsetX(double lineWidth, double paragraphWidth, TextAlignment textAlignment) |
||||
|
{ |
||||
|
if (double.IsPositiveInfinity(paragraphWidth)) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
switch (textAlignment) |
||||
|
{ |
||||
|
case TextAlignment.Center: |
||||
|
return (paragraphWidth - lineWidth) / 2; |
||||
|
|
||||
|
case TextAlignment.Right: |
||||
|
return paragraphWidth - lineWidth; |
||||
|
|
||||
|
default: |
||||
|
return 0.0f; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a metric for a <see cref="TextLine"/> objects,
|
||||
|
/// that holds information about ascent, descent, line gap, size and origin of the text line.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextLineMetrics |
||||
|
{ |
||||
|
public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap) |
||||
|
{ |
||||
|
Ascent = ascent; |
||||
|
Descent = descent; |
||||
|
LineGap = lineGap; |
||||
|
Size = new Size(width, descent - ascent + lineGap); |
||||
|
BaselineOrigin = new Point(xOrigin, -ascent); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the overall recommended distance above the baseline.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The ascent.
|
||||
|
/// </value>
|
||||
|
public double Ascent { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the overall recommended distance under the baseline.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The descent.
|
||||
|
/// </value>
|
||||
|
public double Descent { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the overall recommended additional space between two lines of text.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The leading.
|
||||
|
/// </value>
|
||||
|
public double LineGap { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the size of the text line.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The size.
|
||||
|
/// </value>
|
||||
|
public Size Size { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the baseline origin.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The baseline origin.
|
||||
|
/// </value>
|
||||
|
public Point BaselineOrigin { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the text line metrics.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text runs.</param>
|
||||
|
/// <param name="paragraphWidth">The paragraph width.</param>
|
||||
|
/// <param name="textAlignment">The text alignment.</param>
|
||||
|
/// <returns></returns>
|
||||
|
public static TextLineMetrics Create(IEnumerable<TextRun> textRuns, double paragraphWidth, TextAlignment textAlignment) |
||||
|
{ |
||||
|
var lineWidth = 0.0; |
||||
|
var ascent = 0.0; |
||||
|
var descent = 0.0; |
||||
|
var lineGap = 0.0; |
||||
|
|
||||
|
foreach (var textRun in textRuns) |
||||
|
{ |
||||
|
var shapedRun = (ShapedTextRun)textRun; |
||||
|
|
||||
|
lineWidth += shapedRun.Bounds.Width; |
||||
|
|
||||
|
var textFormat = textRun.Style.TextFormat; |
||||
|
|
||||
|
if (ascent > textRun.Style.TextFormat.FontMetrics.Ascent) |
||||
|
{ |
||||
|
ascent = textFormat.FontMetrics.Ascent; |
||||
|
} |
||||
|
|
||||
|
if (descent < textFormat.FontMetrics.Descent) |
||||
|
{ |
||||
|
descent = textFormat.FontMetrics.Descent; |
||||
|
} |
||||
|
|
||||
|
if (lineGap < textFormat.FontMetrics.LineGap) |
||||
|
{ |
||||
|
lineGap = textFormat.FontMetrics.LineGap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, textAlignment); |
||||
|
|
||||
|
return new TextLineMetrics(lineWidth, xOrigin, ascent, descent, lineGap); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides a set of properties that are used during the paragraph layout.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextParagraphProperties |
||||
|
{ |
||||
|
public TextParagraphProperties( |
||||
|
TextStyle defaultTextStyle, |
||||
|
TextAlignment textAlignment = TextAlignment.Left, |
||||
|
TextWrapping textWrapping = TextWrapping.NoWrap, |
||||
|
TextTrimming textTrimming = TextTrimming.None) |
||||
|
{ |
||||
|
DefaultTextStyle = defaultTextStyle; |
||||
|
TextAlignment = textAlignment; |
||||
|
TextWrapping = textWrapping; |
||||
|
TextTrimming = textTrimming; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the default text style.
|
||||
|
/// </summary>
|
||||
|
public TextStyle DefaultTextStyle { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text alignment.
|
||||
|
/// </summary>
|
||||
|
public TextAlignment TextAlignment { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text wrapping.
|
||||
|
/// </summary>
|
||||
|
public TextWrapping TextWrapping { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text trimming.
|
||||
|
/// </summary>
|
||||
|
public TextTrimming TextTrimming { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// References a portion of a text buffer.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextPointer |
||||
|
{ |
||||
|
public TextPointer(int start, int length) |
||||
|
{ |
||||
|
Start = start; |
||||
|
Length = length; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the start.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The start.
|
||||
|
/// </value>
|
||||
|
public int Start { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the length.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The length.
|
||||
|
/// </value>
|
||||
|
public int Length { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the end.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The end.
|
||||
|
/// </value>
|
||||
|
public int End => Start + Length - 1; |
||||
|
|
||||
|
/// <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="TextPointer"/> that contains the specified number of elements from the start of this slice.</returns>
|
||||
|
public TextPointer Take(int length) |
||||
|
{ |
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new TextPointer(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="TextPointer"/> that contains the elements that occur after the specified index in this slice.</returns>
|
||||
|
public TextPointer Skip(int length) |
||||
|
{ |
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new TextPointer(Start + length, Length - length); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Diagnostics; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a portion of a <see cref="TextLine"/> object.
|
||||
|
/// </summary>
|
||||
|
[DebuggerTypeProxy(typeof(TextRunDebuggerProxy))] |
||||
|
public abstract class TextRun |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the text run's text.
|
||||
|
/// </summary>
|
||||
|
public ReadOnlySlice<char> Text { get; protected set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text run's style.
|
||||
|
/// </summary>
|
||||
|
public TextStyle Style { get; protected set; } |
||||
|
|
||||
|
private class TextRunDebuggerProxy |
||||
|
{ |
||||
|
private readonly TextRun _textRun; |
||||
|
|
||||
|
public TextRunDebuggerProxy(TextRun textRun) |
||||
|
{ |
||||
|
_textRun = textRun; |
||||
|
} |
||||
|
|
||||
|
public string Text |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
unsafe |
||||
|
{ |
||||
|
fixed (char* charsPtr = _textRun.Text.Buffer.Span) |
||||
|
{ |
||||
|
return new string(charsPtr, 0, _textRun.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public TextStyle Style => _textRun.Style; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A class that is responsible for text shaping.
|
||||
|
/// </summary>
|
||||
|
public class TextShaper |
||||
|
{ |
||||
|
private readonly ITextShaperImpl _platformImpl; |
||||
|
|
||||
|
public TextShaper(ITextShaperImpl platformImpl) |
||||
|
{ |
||||
|
_platformImpl = platformImpl; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the current text shaper.
|
||||
|
/// </summary>
|
||||
|
public static TextShaper Current |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
var current = AvaloniaLocator.Current.GetService<TextShaper>(); |
||||
|
|
||||
|
if (current != null) |
||||
|
{ |
||||
|
return current; |
||||
|
} |
||||
|
|
||||
|
var textShaperImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>(); |
||||
|
|
||||
|
if (textShaperImpl == null) |
||||
|
throw new InvalidOperationException("No text shaper implementation was registered."); |
||||
|
|
||||
|
current = new TextShaper(textShaperImpl); |
||||
|
|
||||
|
AvaloniaLocator.CurrentMutable.Bind<TextShaper>().ToConstant(current); |
||||
|
|
||||
|
return current; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Shapes the specified text and returns a resulting glyph run.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The text.</param>
|
||||
|
/// <param name="textFormat">The text format.</param>
|
||||
|
/// <returns>A shaped glyph run.</returns>
|
||||
|
public GlyphRun ShapeText(ReadOnlySlice<char> text, TextFormat textFormat) |
||||
|
{ |
||||
|
return _platformImpl.ShapeText(text, textFormat); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Media.Immutable; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Unique text formatting properties that effect the styling of a text.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextStyle |
||||
|
{ |
||||
|
public TextStyle(Typeface typeface, double fontRenderingEmSize = 12, IBrush foreground = null, |
||||
|
ImmutableTextDecoration[] textDecorations = null) |
||||
|
: this(new TextFormat(typeface, fontRenderingEmSize), foreground, textDecorations) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public TextStyle(TextFormat textFormat, IBrush foreground = null, |
||||
|
ImmutableTextDecoration[] textDecorations = null) |
||||
|
{ |
||||
|
TextFormat = textFormat; |
||||
|
Foreground = foreground; |
||||
|
TextDecorations = textDecorations; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text format.
|
||||
|
/// </summary>
|
||||
|
public TextFormat TextFormat { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the foreground.
|
||||
|
/// </summary>
|
||||
|
public IBrush Foreground { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text decorations.
|
||||
|
/// </summary>
|
||||
|
public ImmutableTextDecoration[] TextDecorations { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a text run's style and is used during the layout process of the <see cref="TextFormatter"/>.
|
||||
|
/// </summary>
|
||||
|
public readonly struct TextStyleRun |
||||
|
{ |
||||
|
public TextStyleRun(TextPointer textPointer, TextStyle style) |
||||
|
{ |
||||
|
TextPointer = textPointer; |
||||
|
Style = style; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text pointer.
|
||||
|
/// </summary>
|
||||
|
public TextPointer TextPointer { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text style.
|
||||
|
/// </summary>
|
||||
|
public TextStyle Style { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum BiDiClass |
||||
|
{ |
||||
|
ArabicLetter, //AL
|
||||
|
ArabicNumber, //AN
|
||||
|
ParagraphSeparator, //B
|
||||
|
BoundaryNeutral, //BN
|
||||
|
CommonSeparator, //CS
|
||||
|
EuropeanNumber, //EN
|
||||
|
EuropeanSeparator, //ES
|
||||
|
EuropeanTerminator, //ET
|
||||
|
FirstStrongIsolate, //FSI
|
||||
|
LeftToRight, //L
|
||||
|
LeftToRightEmbedding, //LRE
|
||||
|
LeftToRightIsolate, //LRI
|
||||
|
LeftToRightOverride, //LRO
|
||||
|
NonspacingMark, //NSM
|
||||
|
OtherNeutral, //ON
|
||||
|
PopDirectionalFormat, //PDF
|
||||
|
PopDirectionalIsolate, //PDI
|
||||
|
RightToLeft, //R
|
||||
|
RightToLeftEmbedding, //RLE
|
||||
|
RightToLeftIsolate, //RLI
|
||||
|
RightToLeftOverride, //RLO
|
||||
|
SegmentSeparator, //S
|
||||
|
WhiteSpace, //WS
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.IO; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal static class BinaryReaderExtensions |
||||
|
{ |
||||
|
public static int ReadInt32BE(this BinaryReader reader) |
||||
|
{ |
||||
|
var bytes = reader.ReadBytes(4); |
||||
|
|
||||
|
if (BitConverter.IsLittleEndian) |
||||
|
{ |
||||
|
Array.Reverse(bytes); |
||||
|
} |
||||
|
|
||||
|
return BitConverter.ToInt32(bytes, 0); |
||||
|
} |
||||
|
|
||||
|
public static uint ReadUInt32BE(this BinaryReader reader) |
||||
|
{ |
||||
|
var bytes = reader.ReadBytes(4); |
||||
|
|
||||
|
if (BitConverter.IsLittleEndian) |
||||
|
{ |
||||
|
Array.Reverse(bytes); |
||||
|
} |
||||
|
|
||||
|
return BitConverter.ToUInt32(bytes, 0); |
||||
|
} |
||||
|
|
||||
|
public static void WriteBE(this BinaryWriter writer, int value) |
||||
|
{ |
||||
|
var bytes = BitConverter.GetBytes(value); |
||||
|
|
||||
|
if (BitConverter.IsLittleEndian) |
||||
|
{ |
||||
|
Array.Reverse(bytes); |
||||
|
} |
||||
|
|
||||
|
writer.Write(bytes); |
||||
|
} |
||||
|
|
||||
|
public static void WriteBE(this BinaryWriter writer, uint value) |
||||
|
{ |
||||
|
var bytes = BitConverter.GetBytes(value); |
||||
|
|
||||
|
if (BitConverter.IsLittleEndian) |
||||
|
{ |
||||
|
Array.Reverse(bytes); |
||||
|
} |
||||
|
|
||||
|
writer.Write(bytes); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal static class BreakPairTable |
||||
|
{ |
||||
|
private static readonly byte[][] s_breakPairTable = |
||||
|
{ |
||||
|
new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4}, |
||||
|
new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, |
||||
|
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,1,0,1,1,0,0,4,2,4,0,0,0,0,0,0,1,1,1}, |
||||
|
}; |
||||
|
|
||||
|
public static PairBreakType Map(LineBreakClass first, LineBreakClass second) |
||||
|
{ |
||||
|
return (PairBreakType)s_breakPairTable[(int)first][(int)second]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal enum PairBreakType : byte |
||||
|
{ |
||||
|
DI = 0, // Direct break opportunity
|
||||
|
IN = 1, // Indirect break opportunity
|
||||
|
CI = 2, // Indirect break opportunity for combining marks
|
||||
|
CP = 3, // Prohibited break for combining marks
|
||||
|
PR = 4 // Prohibited break
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,169 @@ |
|||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public readonly struct Codepoint |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The replacement codepoint that is used for non supported values.
|
||||
|
/// </summary>
|
||||
|
public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); |
||||
|
|
||||
|
private readonly int _value; |
||||
|
|
||||
|
public Codepoint(int value) |
||||
|
{ |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Unicode.GeneralCategory"/>.
|
||||
|
/// </summary>
|
||||
|
public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(_value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Unicode.Script"/>.
|
||||
|
/// </summary>
|
||||
|
public Script Script => UnicodeData.GetScript(_value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Unicode.BiDiClass"/>.
|
||||
|
/// </summary>
|
||||
|
public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(_value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Unicode.LineBreakClass"/>.
|
||||
|
/// </summary>
|
||||
|
public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(_value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="GraphemeBreakClass"/>.
|
||||
|
/// </summary>
|
||||
|
public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(_value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether this <see cref="Codepoint"/> is a break char.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// <c>true</c> if [is break character]; otherwise, <c>false</c>.
|
||||
|
/// </returns>
|
||||
|
public bool IsBreakChar |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
switch (_value) |
||||
|
{ |
||||
|
case '\u000A': |
||||
|
case '\u000B': |
||||
|
case '\u000C': |
||||
|
case '\u000D': |
||||
|
case '\u0085': |
||||
|
case '\u2028': |
||||
|
case '\u2029': |
||||
|
return true; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether this <see cref="Codepoint"/> is white space.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// <c>true</c> if [is whitespace]; otherwise, <c>false</c>.
|
||||
|
/// </returns>
|
||||
|
public bool IsWhiteSpace |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
switch (GeneralCategory) |
||||
|
{ |
||||
|
case GeneralCategory.Control: |
||||
|
case GeneralCategory.NonspacingMark: |
||||
|
case GeneralCategory.Format: |
||||
|
case GeneralCategory.SpaceSeparator: |
||||
|
case GeneralCategory.SpacingMark: |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static implicit operator int(Codepoint codepoint) |
||||
|
{ |
||||
|
return codepoint._value; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator uint(Codepoint codepoint) |
||||
|
{ |
||||
|
return (uint)codepoint._value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the <see cref="Codepoint"/> at specified position.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The buffer to read from.</param>
|
||||
|
/// <param name="index">The index to read at.</param>
|
||||
|
/// <param name="count">The count of character that were read.</param>
|
||||
|
/// <returns></returns>
|
||||
|
public static Codepoint ReadAt(ReadOnlySlice<char> text, int index, out int count) |
||||
|
{ |
||||
|
count = 1; |
||||
|
|
||||
|
if (index > text.End) |
||||
|
{ |
||||
|
return ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
var code = text[index]; |
||||
|
|
||||
|
ushort hi, low; |
||||
|
|
||||
|
//# High surrogate
|
||||
|
if (0xD800 <= code && code <= 0xDBFF) |
||||
|
{ |
||||
|
hi = code; |
||||
|
|
||||
|
if (index + 1 == text.Length) |
||||
|
{ |
||||
|
return ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
low = text[index + 1]; |
||||
|
|
||||
|
if (0xDC00 <= low && low <= 0xDFFF) |
||||
|
{ |
||||
|
count = 2; |
||||
|
return new Codepoint((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000); |
||||
|
} |
||||
|
|
||||
|
return ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
//# Low surrogate
|
||||
|
if (0xDC00 <= code && code <= 0xDFFF) |
||||
|
{ |
||||
|
if (index == 0) |
||||
|
{ |
||||
|
return ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
hi = text[index - 1]; |
||||
|
|
||||
|
low = code; |
||||
|
|
||||
|
if (0xD800 <= hi && hi <= 0xDBFF) |
||||
|
{ |
||||
|
count = 2; |
||||
|
return new Codepoint((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000); |
||||
|
} |
||||
|
|
||||
|
return ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
return new Codepoint(code); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal ref struct CodepointEnumerator |
||||
|
{ |
||||
|
private ReadOnlySlice<char> _text; |
||||
|
|
||||
|
public CodepointEnumerator(ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
_text = text; |
||||
|
Current = Codepoint.ReplacementCodepoint; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the current <see cref="Codepoint"/>.
|
||||
|
/// </summary>
|
||||
|
public Codepoint Current { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Moves to the next <see cref="Codepoint"/>.
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
if (_text.IsEmpty) |
||||
|
{ |
||||
|
Current = Codepoint.ReplacementCodepoint; |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Current = Codepoint.ReadAt(_text, 0, out var count); |
||||
|
|
||||
|
_text = _text.Skip(count); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum GeneralCategory |
||||
|
{ |
||||
|
Other, //C# Cc | Cf | Cn | Co | Cs
|
||||
|
Control, //Cc
|
||||
|
Format, //Cf
|
||||
|
Unassigned, //Cn
|
||||
|
PrivateUse, //Co
|
||||
|
Surrogate, //Cs
|
||||
|
Letter, //L# Ll | Lm | Lo | Lt | Lu
|
||||
|
CasedLetter, //LC# Ll | Lt | Lu
|
||||
|
LowercaseLetter, //Ll
|
||||
|
ModifierLetter, //Lm
|
||||
|
OtherLetter, //Lo
|
||||
|
TitlecaseLetter, //Lt
|
||||
|
UppercaseLetter, //Lu
|
||||
|
Mark, //M
|
||||
|
SpacingMark, //Mc
|
||||
|
EnclosingMark, //Me
|
||||
|
NonspacingMark, //Mn
|
||||
|
Number, //N# Nd | Nl | No
|
||||
|
DecimalNumber, //Nd
|
||||
|
LetterNumber, //Nl
|
||||
|
OtherNumber, //No
|
||||
|
Punctuation, //P
|
||||
|
ConnectorPunctuation, //Pc
|
||||
|
DashPunctuation, //Pd
|
||||
|
ClosePunctuation, //Pe
|
||||
|
FinalPunctuation, //Pf
|
||||
|
InitialPunctuation, //Pi
|
||||
|
OtherPunctuation, //Po
|
||||
|
OpenPunctuation, //Ps
|
||||
|
Symbol, //S# Sc | Sk | Sm | So
|
||||
|
CurrencySymbol, //Sc
|
||||
|
ModifierSymbol, //Sk
|
||||
|
MathSymbol, //Sm
|
||||
|
OtherSymbol, //So
|
||||
|
Separator, //Z# Zl | Zp | Zs
|
||||
|
LineSeparator, //Zl
|
||||
|
ParagraphSeparator, //Zp
|
||||
|
SpaceSeparator, //Zs
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents the smallest unit of a writing system of any given language.
|
||||
|
/// </summary>
|
||||
|
public readonly struct Grapheme |
||||
|
{ |
||||
|
public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
FirstCodepoint = firstCodepoint; |
||||
|
Text = text; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The first <see cref="Codepoint"/> of the grapheme cluster.
|
||||
|
/// </summary>
|
||||
|
public Codepoint FirstCodepoint { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The text that is representing the <see cref="Grapheme"/>.
|
||||
|
/// </summary>
|
||||
|
public ReadOnlySlice<char> Text { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum GraphemeBreakClass |
||||
|
{ |
||||
|
Control, //CN
|
||||
|
CR, //CR
|
||||
|
EBase, //EB
|
||||
|
EBaseGAZ, //EBG
|
||||
|
EModifier, //EM
|
||||
|
Extend, //EX
|
||||
|
GlueAfterZwj, //GAZ
|
||||
|
L, //L
|
||||
|
LF, //LF
|
||||
|
LV, //LV
|
||||
|
LVT, //LVT
|
||||
|
Prepend, //PP
|
||||
|
RegionalIndicator, //RI
|
||||
|
SpacingMark, //SM
|
||||
|
T, //T
|
||||
|
V, //V
|
||||
|
Other, //XX
|
||||
|
ZWJ, //ZWJ
|
||||
|
ExtendedPictographic |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,263 @@ |
|||||
|
// This source file is adapted from the .NET cross-platform runtime project.
|
||||
|
// (https://github.com/dotnet/runtime/)
|
||||
|
//
|
||||
|
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
||||
|
|
||||
|
using System.Runtime.InteropServices; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public ref struct GraphemeEnumerator |
||||
|
{ |
||||
|
private ReadOnlySlice<char> _text; |
||||
|
|
||||
|
public GraphemeEnumerator(ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
_text = text; |
||||
|
Current = default; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the current <see cref="Grapheme"/>.
|
||||
|
/// </summary>
|
||||
|
public Grapheme Current { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Moves to the next <see cref="Grapheme"/>.
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
if (_text.IsEmpty) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules.
|
||||
|
|
||||
|
var processor = new Processor(_text); |
||||
|
|
||||
|
processor.MoveNext(); |
||||
|
|
||||
|
var firstCodepoint = processor.CurrentCodepoint; |
||||
|
|
||||
|
// First, consume as many Prepend scalars as we can (rule GB9b).
|
||||
|
while (processor.CurrentType == GraphemeBreakClass.Prepend) |
||||
|
{ |
||||
|
processor.MoveNext(); |
||||
|
} |
||||
|
|
||||
|
// Next, make sure we're not about to violate control character restrictions.
|
||||
|
// Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5).
|
||||
|
if (processor.CurrentCodeUnitOffset > 0) |
||||
|
{ |
||||
|
if (processor.CurrentType == GraphemeBreakClass.Control |
||||
|
|| processor.CurrentType == GraphemeBreakClass.CR |
||||
|
|| processor.CurrentType == GraphemeBreakClass.LF) |
||||
|
{ |
||||
|
goto Return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Now begin the main state machine.
|
||||
|
|
||||
|
var previousClusterBreakType = processor.CurrentType; |
||||
|
|
||||
|
processor.MoveNext(); |
||||
|
|
||||
|
switch (previousClusterBreakType) |
||||
|
{ |
||||
|
case GraphemeBreakClass.CR: |
||||
|
if (processor.CurrentType != GraphemeBreakClass.LF) |
||||
|
{ |
||||
|
goto Return; // rules GB3 & GB4 (only <LF> can follow <CR>)
|
||||
|
} |
||||
|
|
||||
|
processor.MoveNext(); |
||||
|
goto case GraphemeBreakClass.LF; |
||||
|
|
||||
|
case GraphemeBreakClass.Control: |
||||
|
case GraphemeBreakClass.LF: |
||||
|
goto Return; // rule GB4 (no data after Control | LF)
|
||||
|
|
||||
|
case GraphemeBreakClass.L: |
||||
|
if (processor.CurrentType == GraphemeBreakClass.L) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB6 (L x L)
|
||||
|
goto case GraphemeBreakClass.L; |
||||
|
} |
||||
|
else if (processor.CurrentType == GraphemeBreakClass.V) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB6 (L x V)
|
||||
|
goto case GraphemeBreakClass.V; |
||||
|
} |
||||
|
else if (processor.CurrentType == GraphemeBreakClass.LV) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB6 (L x LV)
|
||||
|
goto case GraphemeBreakClass.LV; |
||||
|
} |
||||
|
else if (processor.CurrentType == GraphemeBreakClass.LVT) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB6 (L x LVT)
|
||||
|
goto case GraphemeBreakClass.LVT; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case GraphemeBreakClass.LV: |
||||
|
case GraphemeBreakClass.V: |
||||
|
if (processor.CurrentType == GraphemeBreakClass.V) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB7 (LV | V x V)
|
||||
|
goto case GraphemeBreakClass.V; |
||||
|
} |
||||
|
else if (processor.CurrentType == GraphemeBreakClass.T) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB7 (LV | V x T)
|
||||
|
goto case GraphemeBreakClass.T; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case GraphemeBreakClass.LVT: |
||||
|
case GraphemeBreakClass.T: |
||||
|
if (processor.CurrentType == GraphemeBreakClass.T) |
||||
|
{ |
||||
|
processor.MoveNext(); // rule GB8 (LVT | T x T)
|
||||
|
goto case GraphemeBreakClass.T; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case GraphemeBreakClass.ExtendedPictographic: |
||||
|
// Attempt processing extended pictographic (rules GB11, GB9).
|
||||
|
// First, drain any Extend scalars that might exist
|
||||
|
while (processor.CurrentType == GraphemeBreakClass.Extend) |
||||
|
{ |
||||
|
processor.MoveNext(); |
||||
|
} |
||||
|
|
||||
|
// Now see if there's a ZWJ + extended pictograph again.
|
||||
|
if (processor.CurrentType != GraphemeBreakClass.ZWJ) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
processor.MoveNext(); |
||||
|
if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
processor.MoveNext(); |
||||
|
goto case GraphemeBreakClass.ExtendedPictographic; |
||||
|
|
||||
|
case GraphemeBreakClass.RegionalIndicator: |
||||
|
// We've consumed a single RI scalar. Try to consume another (to make it a pair).
|
||||
|
|
||||
|
if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) |
||||
|
{ |
||||
|
processor.MoveNext(); |
||||
|
} |
||||
|
|
||||
|
// Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers.
|
||||
|
|
||||
|
break; // nothing but trailers after the final RI
|
||||
|
|
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// rules GB9, GB9a
|
||||
|
while (processor.CurrentType == GraphemeBreakClass.Extend |
||||
|
|| processor.CurrentType == GraphemeBreakClass.ZWJ |
||||
|
|| processor.CurrentType == GraphemeBreakClass.SpacingMark) |
||||
|
{ |
||||
|
processor.MoveNext(); |
||||
|
} |
||||
|
|
||||
|
Return: |
||||
|
|
||||
|
var text = _text.Take(processor.CurrentCodeUnitOffset); |
||||
|
|
||||
|
Current = new Grapheme(firstCodepoint, text); |
||||
|
|
||||
|
_text = _text.Skip(processor.CurrentCodeUnitOffset); |
||||
|
|
||||
|
return true; // rules GB2, GB999
|
||||
|
} |
||||
|
|
||||
|
[StructLayout(LayoutKind.Auto)] |
||||
|
private ref struct Processor |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _buffer; |
||||
|
private int _codeUnitLengthOfCurrentScalar; |
||||
|
|
||||
|
internal Processor(ReadOnlySlice<char> buffer) |
||||
|
{ |
||||
|
_buffer = buffer; |
||||
|
_codeUnitLengthOfCurrentScalar = 0; |
||||
|
CurrentCodepoint = Codepoint.ReplacementCodepoint; |
||||
|
CurrentType = GraphemeBreakClass.Other; |
||||
|
CurrentCodeUnitOffset = 0; |
||||
|
} |
||||
|
|
||||
|
public int CurrentCodeUnitOffset { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Will be <see cref="GraphemeBreakClass.Other"/> if invalid data or EOF reached.
|
||||
|
/// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
|
||||
|
/// </summary>
|
||||
|
public GraphemeBreakClass CurrentType { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the currently processed <see cref="Codepoint"/>.
|
||||
|
/// </summary>
|
||||
|
public Codepoint CurrentCodepoint { get; private set; } |
||||
|
|
||||
|
public void MoveNext() |
||||
|
{ |
||||
|
// For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
|
||||
|
// the decoder's default behavior of interpreting these ill-formed subsequences as
|
||||
|
// equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
|
||||
|
// of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
|
||||
|
// See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
|
||||
|
// This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
|
||||
|
// https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
|
||||
|
// has the line "D800..DFFF ; Control # Cs [2048] <surrogate-D800>..<surrogate-DFFF>",
|
||||
|
// but starting with Unicode 12.0 that line has been removed.
|
||||
|
//
|
||||
|
// If a later version of the Unicode Standard further modifies this guidance we should reflect
|
||||
|
// that here.
|
||||
|
|
||||
|
if (CurrentCodeUnitOffset == _buffer.Length) |
||||
|
{ |
||||
|
CurrentCodepoint = Codepoint.ReplacementCodepoint; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar; |
||||
|
|
||||
|
if (CurrentCodeUnitOffset < _buffer.Length) |
||||
|
{ |
||||
|
CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset, |
||||
|
out _codeUnitLengthOfCurrentScalar); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
CurrentCodepoint = Codepoint.ReplacementCodepoint; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
CurrentType = CurrentCodepoint.GraphemeBreakClass; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.
|
||||
|
//
|
||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Information about a potential line break position
|
||||
|
/// </summary>
|
||||
|
[DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] |
||||
|
public readonly struct LineBreak |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Constructor
|
||||
|
/// </summary>
|
||||
|
/// <param name="positionMeasure">The code point index to measure to</param>
|
||||
|
/// <param name="positionWrap">The code point index to actually break the line at</param>
|
||||
|
/// <param name="required">True if this is a required line break; otherwise false</param>
|
||||
|
public LineBreak(int positionMeasure, int positionWrap, bool required = false) |
||||
|
{ |
||||
|
PositionMeasure = positionMeasure; |
||||
|
PositionWrap = positionWrap; |
||||
|
Required = required; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The break position, before any trailing whitespace
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This doesn't include trailing whitespace
|
||||
|
/// </remarks>
|
||||
|
public int PositionMeasure { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The break position, after any trailing whitespace
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This includes trailing whitespace
|
||||
|
/// </remarks>
|
||||
|
public int PositionWrap { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// True if there should be a forced line break here
|
||||
|
/// </summary>
|
||||
|
public bool Required { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum LineBreakClass |
||||
|
{ |
||||
|
OpenPunctuation, //OP
|
||||
|
ClosePunctuation, //CL
|
||||
|
CloseParenthesis, //CP
|
||||
|
Quotation, //QU
|
||||
|
Glue, //GL
|
||||
|
Nonstarter, //NS
|
||||
|
Exclamation, //EX
|
||||
|
BreakSymbols, //SY
|
||||
|
InfixNumeric, //IS
|
||||
|
PrefixNumeric, //PR
|
||||
|
PostfixNumeric, //PO
|
||||
|
Numeric, //NU
|
||||
|
Alphabetic, //AL
|
||||
|
HebrewLetter, //HL
|
||||
|
Ideographic, //ID
|
||||
|
Inseparable, //IN
|
||||
|
Hyphen, //HY
|
||||
|
BreakAfter, //BA
|
||||
|
BreakBefore, //BB
|
||||
|
BreakBoth, //B2
|
||||
|
ZWSpace, //ZW
|
||||
|
CombiningMark, //CM
|
||||
|
WordJoiner, //WJ
|
||||
|
H2, //H2
|
||||
|
H3, //H3
|
||||
|
JL, //JL
|
||||
|
JV, //JV
|
||||
|
JT, //JT
|
||||
|
RegionalIndicator, //RI
|
||||
|
EBase, //EB
|
||||
|
EModifier, //EM
|
||||
|
ZWJ, //ZWJ
|
||||
|
|
||||
|
Ambiguous, //AI
|
||||
|
MandatoryBreak, //BK
|
||||
|
ContingentBreak, //CB
|
||||
|
ConditionalJapaneseStarter, //CJ
|
||||
|
CarriageReturn, //CR
|
||||
|
LineFeed, //LF
|
||||
|
NextLine, //NL
|
||||
|
ComplexContext, //SA
|
||||
|
Surrogate, //SG
|
||||
|
Space, //SP
|
||||
|
Unknown, //XX
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,243 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.
|
||||
|
//
|
||||
|
// Ported from: https://github.com/foliojs/linebreak
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Implementation of the Unicode Line Break Algorithm
|
||||
|
/// </summary>
|
||||
|
public ref struct LineBreakEnumerator |
||||
|
{ |
||||
|
// State
|
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private int _pos; |
||||
|
private int _lastPos; |
||||
|
private LineBreakClass? _curClass; |
||||
|
private LineBreakClass? _nextClass; |
||||
|
|
||||
|
public LineBreakEnumerator(ReadOnlySlice<char> text) |
||||
|
{ |
||||
|
_text = text; |
||||
|
_pos = 0; |
||||
|
_lastPos = 0; |
||||
|
_curClass = null; |
||||
|
_nextClass = null; |
||||
|
Current = default; |
||||
|
} |
||||
|
|
||||
|
public LineBreak Current { get; private set; } |
||||
|
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
// get the first char if we're at the beginning of the string
|
||||
|
if (!_curClass.HasValue) |
||||
|
{ |
||||
|
_curClass = PeekCharClass() == LineBreakClass.Space ? LineBreakClass.WordJoiner : MapFirst(ReadCharClass()); |
||||
|
} |
||||
|
|
||||
|
while (_pos < _text.Length) |
||||
|
{ |
||||
|
_lastPos = _pos; |
||||
|
var lastClass = _nextClass; |
||||
|
_nextClass = ReadCharClass(); |
||||
|
|
||||
|
// explicit newline
|
||||
|
if (_curClass.HasValue && (_curClass == LineBreakClass.MandatoryBreak || _curClass == LineBreakClass.CarriageReturn && _nextClass != LineBreakClass.LineFeed)) |
||||
|
{ |
||||
|
_curClass = MapFirst(MapClass(_nextClass.Value)); |
||||
|
Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// handle classes not handled by the pair table
|
||||
|
LineBreakClass? cur = null; |
||||
|
switch (_nextClass.Value) |
||||
|
{ |
||||
|
case LineBreakClass.Space: |
||||
|
cur = _curClass; |
||||
|
break; |
||||
|
|
||||
|
case LineBreakClass.MandatoryBreak: |
||||
|
case LineBreakClass.LineFeed: |
||||
|
case LineBreakClass.NextLine: |
||||
|
cur = LineBreakClass.MandatoryBreak; |
||||
|
break; |
||||
|
|
||||
|
case LineBreakClass.CarriageReturn: |
||||
|
cur = LineBreakClass.CarriageReturn; |
||||
|
break; |
||||
|
|
||||
|
case LineBreakClass.ContingentBreak: |
||||
|
cur = LineBreakClass.BreakAfter; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (cur != null) |
||||
|
{ |
||||
|
_curClass = cur; |
||||
|
|
||||
|
if (_nextClass.Value == LineBreakClass.MandatoryBreak) |
||||
|
{ |
||||
|
Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// if not handled already, use the pair table
|
||||
|
var shouldBreak = false; |
||||
|
switch (BreakPairTable.Map(_curClass.Value,_nextClass.Value)) |
||||
|
{ |
||||
|
case PairBreakType.DI: // Direct break
|
||||
|
shouldBreak = true; |
||||
|
break; |
||||
|
|
||||
|
case PairBreakType.IN: // possible indirect break
|
||||
|
shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; |
||||
|
break; |
||||
|
|
||||
|
case PairBreakType.CI: |
||||
|
shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space; |
||||
|
if (!shouldBreak) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case PairBreakType.CP: // prohibited for combining marks
|
||||
|
if (!lastClass.HasValue || lastClass.Value != LineBreakClass.Space) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
_curClass = _nextClass; |
||||
|
|
||||
|
if (shouldBreak) |
||||
|
{ |
||||
|
Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (_pos >= _text.Length) |
||||
|
{ |
||||
|
if (_lastPos < _text.Length) |
||||
|
{ |
||||
|
_lastPos = _text.Length; |
||||
|
var cls = Codepoint.ReadAt(_text, _text.Length - 1, out _).LineBreakClass; |
||||
|
bool required = cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn; |
||||
|
Current = new LineBreak(FindPriorNonWhitespace(_text.Length), _text.Length, required); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private int FindPriorNonWhitespace(int from) |
||||
|
{ |
||||
|
if (from > 0) |
||||
|
{ |
||||
|
var cp = Codepoint.ReadAt(_text, from - 1, out var count); |
||||
|
|
||||
|
var cls = cp.LineBreakClass; |
||||
|
|
||||
|
if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn) |
||||
|
{ |
||||
|
from -= count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
while (from > 0) |
||||
|
{ |
||||
|
var cp = Codepoint.ReadAt(_text, from - 1, out var count); |
||||
|
|
||||
|
var cls = cp.LineBreakClass; |
||||
|
|
||||
|
if (cls == LineBreakClass.Space) |
||||
|
{ |
||||
|
from -= count; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return from; |
||||
|
} |
||||
|
|
||||
|
// Get the next character class
|
||||
|
private LineBreakClass ReadCharClass() |
||||
|
{ |
||||
|
var cp = Codepoint.ReadAt(_text, _pos, out var count); |
||||
|
|
||||
|
_pos += count; |
||||
|
|
||||
|
return MapClass(cp.LineBreakClass); |
||||
|
} |
||||
|
|
||||
|
private LineBreakClass PeekCharClass() |
||||
|
{ |
||||
|
return MapClass(Codepoint.ReadAt(_text, _pos, out _).LineBreakClass); |
||||
|
} |
||||
|
|
||||
|
private static LineBreakClass MapClass(LineBreakClass c) |
||||
|
{ |
||||
|
switch (c) |
||||
|
{ |
||||
|
case LineBreakClass.Ambiguous: |
||||
|
return LineBreakClass.Alphabetic; |
||||
|
|
||||
|
case LineBreakClass.ComplexContext: |
||||
|
case LineBreakClass.Surrogate: |
||||
|
case LineBreakClass.Unknown: |
||||
|
return LineBreakClass.Alphabetic; |
||||
|
|
||||
|
case LineBreakClass.ConditionalJapaneseStarter: |
||||
|
return LineBreakClass.Nonstarter; |
||||
|
|
||||
|
default: |
||||
|
return c; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static LineBreakClass MapFirst(LineBreakClass c) |
||||
|
{ |
||||
|
switch (c) |
||||
|
{ |
||||
|
case LineBreakClass.LineFeed: |
||||
|
case LineBreakClass.NextLine: |
||||
|
return LineBreakClass.MandatoryBreak; |
||||
|
|
||||
|
case LineBreakClass.ContingentBreak: |
||||
|
return LineBreakClass.BreakAfter; |
||||
|
|
||||
|
case LineBreakClass.Space: |
||||
|
return LineBreakClass.WordJoiner; |
||||
|
|
||||
|
default: |
||||
|
return c; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum Script |
||||
|
{ |
||||
|
Adlam, //Adlm
|
||||
|
CaucasianAlbanian, //Aghb
|
||||
|
Ahom, //Ahom
|
||||
|
Arabic, //Arab
|
||||
|
ImperialAramaic, //Armi
|
||||
|
Armenian, //Armn
|
||||
|
Avestan, //Avst
|
||||
|
Balinese, //Bali
|
||||
|
Bamum, //Bamu
|
||||
|
BassaVah, //Bass
|
||||
|
Batak, //Batk
|
||||
|
Bengali, //Beng
|
||||
|
Bhaiksuki, //Bhks
|
||||
|
Bopomofo, //Bopo
|
||||
|
Brahmi, //Brah
|
||||
|
Braille, //Brai
|
||||
|
Buginese, //Bugi
|
||||
|
Buhid, //Buhd
|
||||
|
Chakma, //Cakm
|
||||
|
CanadianAboriginal, //Cans
|
||||
|
Carian, //Cari
|
||||
|
Cham, //Cham
|
||||
|
Cherokee, //Cher
|
||||
|
Coptic, //Copt
|
||||
|
Cypriot, //Cprt
|
||||
|
Cyrillic, //Cyrl
|
||||
|
Devanagari, //Deva
|
||||
|
Dogra, //Dogr
|
||||
|
Deseret, //Dsrt
|
||||
|
Duployan, //Dupl
|
||||
|
EgyptianHieroglyphs, //Egyp
|
||||
|
Elbasan, //Elba
|
||||
|
Elymaic, //Elym
|
||||
|
Ethiopic, //Ethi
|
||||
|
Georgian, //Geor
|
||||
|
Glagolitic, //Glag
|
||||
|
GunjalaGondi, //Gong
|
||||
|
MasaramGondi, //Gonm
|
||||
|
Gothic, //Goth
|
||||
|
Grantha, //Gran
|
||||
|
Greek, //Grek
|
||||
|
Gujarati, //Gujr
|
||||
|
Gurmukhi, //Guru
|
||||
|
Hangul, //Hang
|
||||
|
Han, //Hani
|
||||
|
Hanunoo, //Hano
|
||||
|
Hatran, //Hatr
|
||||
|
Hebrew, //Hebr
|
||||
|
Hiragana, //Hira
|
||||
|
AnatolianHieroglyphs, //Hluw
|
||||
|
PahawhHmong, //Hmng
|
||||
|
NyiakengPuachueHmong, //Hmnp
|
||||
|
KatakanaOrHiragana, //Hrkt
|
||||
|
OldHungarian, //Hung
|
||||
|
OldItalic, //Ital
|
||||
|
Javanese, //Java
|
||||
|
KayahLi, //Kali
|
||||
|
Katakana, //Kana
|
||||
|
Kharoshthi, //Khar
|
||||
|
Khmer, //Khmr
|
||||
|
Khojki, //Khoj
|
||||
|
Kannada, //Knda
|
||||
|
Kaithi, //Kthi
|
||||
|
TaiTham, //Lana
|
||||
|
Lao, //Laoo
|
||||
|
Latin, //Latn
|
||||
|
Lepcha, //Lepc
|
||||
|
Limbu, //Limb
|
||||
|
LinearA, //Lina
|
||||
|
LinearB, //Linb
|
||||
|
Lisu, //Lisu
|
||||
|
Lycian, //Lyci
|
||||
|
Lydian, //Lydi
|
||||
|
Mahajani, //Mahj
|
||||
|
Makasar, //Maka
|
||||
|
Mandaic, //Mand
|
||||
|
Manichaean, //Mani
|
||||
|
Marchen, //Marc
|
||||
|
Medefaidrin, //Medf
|
||||
|
MendeKikakui, //Mend
|
||||
|
MeroiticCursive, //Merc
|
||||
|
MeroiticHieroglyphs, //Mero
|
||||
|
Malayalam, //Mlym
|
||||
|
Modi, //Modi
|
||||
|
Mongolian, //Mong
|
||||
|
Mro, //Mroo
|
||||
|
MeeteiMayek, //Mtei
|
||||
|
Multani, //Mult
|
||||
|
Myanmar, //Mymr
|
||||
|
Nandinagari, //Nand
|
||||
|
OldNorthArabian, //Narb
|
||||
|
Nabataean, //Nbat
|
||||
|
Newa, //Newa
|
||||
|
Nko, //Nkoo
|
||||
|
Nushu, //Nshu
|
||||
|
Ogham, //Ogam
|
||||
|
OlChiki, //Olck
|
||||
|
OldTurkic, //Orkh
|
||||
|
Oriya, //Orya
|
||||
|
Osage, //Osge
|
||||
|
Osmanya, //Osma
|
||||
|
Palmyrene, //Palm
|
||||
|
PauCinHau, //Pauc
|
||||
|
OldPermic, //Perm
|
||||
|
PhagsPa, //Phag
|
||||
|
InscriptionalPahlavi, //Phli
|
||||
|
PsalterPahlavi, //Phlp
|
||||
|
Phoenician, //Phnx
|
||||
|
Miao, //Plrd
|
||||
|
InscriptionalParthian, //Prti
|
||||
|
Rejang, //Rjng
|
||||
|
HanifiRohingya, //Rohg
|
||||
|
Runic, //Runr
|
||||
|
Samaritan, //Samr
|
||||
|
OldSouthArabian, //Sarb
|
||||
|
Saurashtra, //Saur
|
||||
|
SignWriting, //Sgnw
|
||||
|
Shavian, //Shaw
|
||||
|
Sharada, //Shrd
|
||||
|
Siddham, //Sidd
|
||||
|
Khudawadi, //Sind
|
||||
|
Sinhala, //Sinh
|
||||
|
Sogdian, //Sogd
|
||||
|
OldSogdian, //Sogo
|
||||
|
SoraSompeng, //Sora
|
||||
|
Soyombo, //Soyo
|
||||
|
Sundanese, //Sund
|
||||
|
SylotiNagri, //Sylo
|
||||
|
Syriac, //Syrc
|
||||
|
Tagbanwa, //Tagb
|
||||
|
Takri, //Takr
|
||||
|
TaiLe, //Tale
|
||||
|
NewTaiLue, //Talu
|
||||
|
Tamil, //Taml
|
||||
|
Tangut, //Tang
|
||||
|
TaiViet, //Tavt
|
||||
|
Telugu, //Telu
|
||||
|
Tifinagh, //Tfng
|
||||
|
Tagalog, //Tglg
|
||||
|
Thaana, //Thaa
|
||||
|
Thai, //Thai
|
||||
|
Tibetan, //Tibt
|
||||
|
Tirhuta, //Tirh
|
||||
|
Ugaritic, //Ugar
|
||||
|
Vai, //Vaii
|
||||
|
WarangCiti, //Wara
|
||||
|
Wancho, //Wcho
|
||||
|
OldPersian, //Xpeo
|
||||
|
Cuneiform, //Xsux
|
||||
|
Yi, //Yiii
|
||||
|
ZanabazarSquare, //Zanb
|
||||
|
Inherited, //Zinh
|
||||
|
Common, //Zyyy
|
||||
|
Unknown, //Zzzz
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Helper for looking up unicode character class information
|
||||
|
/// </summary>
|
||||
|
internal static class UnicodeData |
||||
|
{ |
||||
|
internal const int CATEGORY_BITS = 6; |
||||
|
internal const int SCRIPT_BITS = 8; |
||||
|
internal const int BIDI_BITS = 5; |
||||
|
internal const int LINEBREAK_BITS = 6; |
||||
|
|
||||
|
internal const int SCRIPT_SHIFT = CATEGORY_BITS; |
||||
|
internal const int BIDI_SHIFT = CATEGORY_BITS + SCRIPT_BITS; |
||||
|
internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS + BIDI_BITS; |
||||
|
|
||||
|
internal const int CATEGORY_MASK = (1 << CATEGORY_BITS) - 1; |
||||
|
internal const int SCRIPT_MASK = (1 << SCRIPT_BITS) - 1; |
||||
|
internal const int BIDI_MASK = (1 << BIDI_BITS) - 1; |
||||
|
internal const int LINEBREAK_MASK = (1 << LINEBREAK_BITS) - 1; |
||||
|
|
||||
|
private static readonly UnicodeTrie s_unicodeDataTrie; |
||||
|
private static readonly UnicodeTrie s_graphemeBreakTrie; |
||||
|
|
||||
|
static UnicodeData() |
||||
|
{ |
||||
|
s_unicodeDataTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.UnicodeData.trie")); |
||||
|
s_graphemeBreakTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.GraphemeBreak.trie")); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="GeneralCategory"/> for a Unicode codepoint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepoint">The codepoint in question.</param>
|
||||
|
/// <returns>The code point's general category.</returns>
|
||||
|
public static GeneralCategory GetGeneralCategory(int codepoint) |
||||
|
{ |
||||
|
var value = s_unicodeDataTrie.Get(codepoint); |
||||
|
|
||||
|
return (GeneralCategory)(value & CATEGORY_MASK); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Script"/> for a Unicode codepoint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepoint">The codepoint in question.</param>
|
||||
|
/// <returns>The code point's script.</returns>
|
||||
|
public static Script GetScript(int codepoint) |
||||
|
{ |
||||
|
var value = s_unicodeDataTrie.Get(codepoint); |
||||
|
|
||||
|
return (Script)((value >> SCRIPT_SHIFT) & SCRIPT_MASK); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="BiDiClass"/> for a Unicode codepoint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepoint">The codepoint in question.</param>
|
||||
|
/// <returns>The code point's biDi class.</returns>
|
||||
|
public static BiDiClass GetBiDiClass(int codepoint) |
||||
|
{ |
||||
|
var value = s_unicodeDataTrie.Get(codepoint); |
||||
|
|
||||
|
return (BiDiClass)((value >> BIDI_SHIFT) & BIDI_MASK); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the line break class for a Unicode codepoint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepoint">The codepoint in question.</param>
|
||||
|
/// <returns>The code point's line break class.</returns>
|
||||
|
public static LineBreakClass GetLineBreakClass(int codepoint) |
||||
|
{ |
||||
|
var value = s_unicodeDataTrie.Get(codepoint); |
||||
|
|
||||
|
return (LineBreakClass)((value >> LINEBREAK_SHIFT) & LINEBREAK_MASK); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the grapheme break type for the Unicode codepoint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepoint">The codepoint in question.</param>
|
||||
|
/// <returns>The code point's grapheme break type.</returns>
|
||||
|
public static GraphemeBreakClass GetGraphemeClusterBreak(int codepoint) |
||||
|
{ |
||||
|
return (GraphemeBreakClass)s_graphemeBreakTrie.Get(codepoint); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
public enum UnicodeGeneralCategory : byte |
||||
|
{ |
||||
|
Other, //C# Cc | Cf | Cn | Co | Cs
|
||||
|
Control, //Cc
|
||||
|
Format, //Cf
|
||||
|
Unassigned, //Cn
|
||||
|
PrivateUse, //Co
|
||||
|
Surrogate, //Cs
|
||||
|
Letter, //L# Ll | Lm | Lo | Lt | Lu
|
||||
|
CasedLetter, //LC# Ll | Lt | Lu
|
||||
|
LowercaseLetter, //Ll
|
||||
|
ModifierLetter, //Lm
|
||||
|
OtherLetter, //Lo
|
||||
|
TitlecaseLetter, //Lt
|
||||
|
UppercaseLetter, //Lu
|
||||
|
Mark, //M
|
||||
|
SpacingMark, //Mc
|
||||
|
EnclosingMark, //Me
|
||||
|
NonspacingMark, //Mn
|
||||
|
Number, //N# Nd | Nl | No
|
||||
|
DecimalNumber, //Nd
|
||||
|
LetterNumber, //Nl
|
||||
|
OtherNumber, //No
|
||||
|
Punctuation, //P
|
||||
|
ConnectorPunctuation, //Pc
|
||||
|
DashPunctuation, //Pd
|
||||
|
ClosePunctuation, //Pe
|
||||
|
FinalPunctuation, //Pf
|
||||
|
InitialPunctuation, //Pi
|
||||
|
OtherPunctuation, //Po
|
||||
|
OpenPunctuation, //Ps
|
||||
|
Symbol, //S# Sc | Sk | Sm | So
|
||||
|
CurrencySymbol, //Sc
|
||||
|
ModifierSymbol, //Sk
|
||||
|
MathSymbol, //Sm
|
||||
|
OtherSymbol, //So
|
||||
|
Separator, //Z# Zl | Zp | Zs
|
||||
|
LineSeparator, //Zl
|
||||
|
ParagraphSeparator, //Zp
|
||||
|
SpaceSeparator, //Zs
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.
|
||||
|
// Ported from: https://github.com/foliojs/unicode-trie
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
using System.IO; |
||||
|
using System.IO.Compression; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal class UnicodeTrie |
||||
|
{ |
||||
|
private readonly int[] _data; |
||||
|
private readonly int _highStart; |
||||
|
private readonly uint _errorValue; |
||||
|
|
||||
|
public UnicodeTrie(Stream stream) |
||||
|
{ |
||||
|
int dataLength; |
||||
|
using (var bw = new BinaryReader(stream, Encoding.UTF8, true)) |
||||
|
{ |
||||
|
_highStart = bw.ReadInt32BE(); |
||||
|
_errorValue = bw.ReadUInt32BE(); |
||||
|
dataLength = bw.ReadInt32BE() / 4; |
||||
|
} |
||||
|
|
||||
|
using (var infl1 = new DeflateStream(stream, CompressionMode.Decompress, true)) |
||||
|
using (var infl2 = new DeflateStream(infl1, CompressionMode.Decompress, true)) |
||||
|
using (var bw = new BinaryReader(infl2, Encoding.UTF8, true)) |
||||
|
{ |
||||
|
_data = new int[dataLength]; |
||||
|
for (int i = 0; i < _data.Length; i++) |
||||
|
{ |
||||
|
_data[i] = bw.ReadInt32(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public UnicodeTrie(byte[] buf) : this(new MemoryStream(buf)) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
internal UnicodeTrie(int[] data, int highStart, uint errorValue) |
||||
|
{ |
||||
|
_data = data; |
||||
|
_highStart = highStart; |
||||
|
_errorValue = errorValue; |
||||
|
} |
||||
|
|
||||
|
internal void Save(Stream stream) |
||||
|
{ |
||||
|
// Write the header info
|
||||
|
using (var bw = new BinaryWriter(stream, Encoding.UTF8, true)) |
||||
|
{ |
||||
|
bw.WriteBE(_highStart); |
||||
|
bw.WriteBE(_errorValue); |
||||
|
bw.WriteBE(_data.Length * 4); |
||||
|
} |
||||
|
|
||||
|
// Double compress the data
|
||||
|
using (var def1 = new DeflateStream(stream, CompressionLevel.Optimal, true)) |
||||
|
using (var def2 = new DeflateStream(def1, CompressionLevel.Optimal, true)) |
||||
|
using (var bw = new BinaryWriter(def2, Encoding.UTF8, true)) |
||||
|
{ |
||||
|
foreach (var v in _data) |
||||
|
{ |
||||
|
bw.Write(v); |
||||
|
} |
||||
|
bw.Flush(); |
||||
|
def2.Flush(); |
||||
|
def1.Flush(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public uint Get(int codePoint) |
||||
|
{ |
||||
|
int index; |
||||
|
if ((codePoint < 0) || (codePoint > 0x10ffff)) |
||||
|
{ |
||||
|
return _errorValue; |
||||
|
} |
||||
|
|
||||
|
if ((codePoint < 0xd800) || ((codePoint > 0xdbff) && (codePoint <= 0xffff))) |
||||
|
{ |
||||
|
// Ordinary BMP code point, excluding leading surrogates.
|
||||
|
// BMP uses a single level lookup. BMP index starts at offset 0 in the index.
|
||||
|
// data is stored in the index array itself.
|
||||
|
index = (_data[codePoint >> UnicodeTrieBuilder.SHIFT_2] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); |
||||
|
return (uint)_data[index]; |
||||
|
} |
||||
|
|
||||
|
if (codePoint <= 0xffff) |
||||
|
{ |
||||
|
// Lead Surrogate Code Point. A Separate index section is stored for
|
||||
|
// lead surrogate code units and code points.
|
||||
|
// The main index has the code unit data.
|
||||
|
// For this function, we need the code point data.
|
||||
|
index = (_data[UnicodeTrieBuilder.LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UnicodeTrieBuilder.SHIFT_2)] << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); |
||||
|
return (uint)_data[index]; |
||||
|
} |
||||
|
|
||||
|
if (codePoint < _highStart) |
||||
|
{ |
||||
|
// Supplemental code point, use two-level lookup.
|
||||
|
index = _data[(UnicodeTrieBuilder.INDEX_1_OFFSET - UnicodeTrieBuilder.OMITTED_BMP_INDEX_1_LENGTH) + (codePoint >> UnicodeTrieBuilder.SHIFT_1)]; |
||||
|
index = _data[index + ((codePoint >> UnicodeTrieBuilder.SHIFT_2) & UnicodeTrieBuilder.INDEX_2_MASK)]; |
||||
|
index = (index << UnicodeTrieBuilder.INDEX_SHIFT) + (codePoint & UnicodeTrieBuilder.DATA_MASK); |
||||
|
return (uint)_data[index]; |
||||
|
} |
||||
|
|
||||
|
return (uint)_data[_data.Length - UnicodeTrieBuilder.DATA_GRANULARITY]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.
|
||||
|
// Ported from: https://github.com/foliojs/unicode-trie
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal partial class UnicodeTrieBuilder |
||||
|
{ |
||||
|
// Shift size for getting the index-1 table offset.
|
||||
|
internal const int SHIFT_1 = 6 + 5; |
||||
|
|
||||
|
// Shift size for getting the index-2 table offset.
|
||||
|
internal const int SHIFT_2 = 5; |
||||
|
|
||||
|
// Difference between the two shift sizes,
|
||||
|
// for getting an index-1 offset from an index-2 offset. 6=11-5
|
||||
|
const int SHIFT_1_2 = SHIFT_1 - SHIFT_2; |
||||
|
|
||||
|
// Number of index-1 entries for the BMP. 32=0x20
|
||||
|
// This part of the index-1 table is omitted from the serialized form.
|
||||
|
internal const int OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> SHIFT_1; |
||||
|
|
||||
|
// Number of code points per index-1 table entry. 2048=0x800
|
||||
|
const int CP_PER_INDEX_1_ENTRY = 1 << SHIFT_1; |
||||
|
|
||||
|
// Number of entries in an index-2 block. 64=0x40
|
||||
|
const int INDEX_2_BLOCK_LENGTH = 1 << SHIFT_1_2; |
||||
|
|
||||
|
// Mask for getting the lower bits for the in-index-2-block offset. */
|
||||
|
internal const int INDEX_2_MASK = INDEX_2_BLOCK_LENGTH - 1; |
||||
|
|
||||
|
// Number of entries in a data block. 32=0x20
|
||||
|
const int DATA_BLOCK_LENGTH = 1 << SHIFT_2; |
||||
|
|
||||
|
// Mask for getting the lower bits for the in-data-block offset.
|
||||
|
internal const int DATA_MASK = DATA_BLOCK_LENGTH - 1; |
||||
|
|
||||
|
// Shift size for shifting left the index array values.
|
||||
|
// Increases possible data size with 16-bit index values at the cost
|
||||
|
// of compactability.
|
||||
|
// This requires data blocks to be aligned by DATA_GRANULARITY.
|
||||
|
internal const int INDEX_SHIFT = 2; |
||||
|
|
||||
|
// The alignment size of a data block. Also the granularity for compaction.
|
||||
|
internal const int DATA_GRANULARITY = 1 << INDEX_SHIFT; |
||||
|
|
||||
|
// The BMP part of the index-2 table is fixed and linear and starts at offset 0.
|
||||
|
// Length=2048=0x800=0x10000>>SHIFT_2.
|
||||
|
const int INDEX_2_OFFSET = 0; |
||||
|
|
||||
|
// The part of the index-2 table for U+D800..U+DBFF stores values for
|
||||
|
// lead surrogate code _units_ not code _points_.
|
||||
|
// Values for lead surrogate code _points_ are indexed with this portion of the table.
|
||||
|
// Length=32=0x20=0x400>>SHIFT_2. (There are 1024=0x400 lead surrogates.)
|
||||
|
internal const int LSCP_INDEX_2_OFFSET = 0x10000 >> SHIFT_2; |
||||
|
const int LSCP_INDEX_2_LENGTH = 0x400 >> SHIFT_2; |
||||
|
|
||||
|
// Count the lengths of both BMP pieces. 2080=0x820
|
||||
|
const int INDEX_2_BMP_LENGTH = LSCP_INDEX_2_OFFSET + LSCP_INDEX_2_LENGTH; |
||||
|
|
||||
|
// The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.
|
||||
|
// Length 32=0x20 for lead bytes C0..DF, regardless of SHIFT_2.
|
||||
|
const int UTF8_2B_INDEX_2_OFFSET = INDEX_2_BMP_LENGTH; |
||||
|
const int UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; // U+0800 is the first code point after 2-byte UTF-8
|
||||
|
|
||||
|
// The index-1 table, only used for supplementary code points, at offset 2112=0x840.
|
||||
|
// Variable length, for code points up to highStart, where the last single-value range starts.
|
||||
|
// Maximum length 512=0x200=0x100000>>SHIFT_1.
|
||||
|
// (For 0x100000 supplementary code points U+10000..U+10ffff.)
|
||||
|
//
|
||||
|
// The part of the index-2 table for supplementary code points starts
|
||||
|
// after this index-1 table.
|
||||
|
//
|
||||
|
// Both the index-1 table and the following part of the index-2 table
|
||||
|
// are omitted completely if there is only BMP data.
|
||||
|
internal const int INDEX_1_OFFSET = UTF8_2B_INDEX_2_OFFSET + UTF8_2B_INDEX_2_LENGTH; |
||||
|
const int MAX_INDEX_1_LENGTH = 0x100000 >> SHIFT_1; |
||||
|
|
||||
|
// The illegal-UTF-8 data block follows the ASCII block, at offset 128=0x80.
|
||||
|
// Used with linear access for single bytes 0..0xbf for simple error handling.
|
||||
|
// Length 64=0x40, not DATA_BLOCK_LENGTH.
|
||||
|
const int BAD_UTF8_DATA_OFFSET = 0x80; |
||||
|
|
||||
|
// The start of non-linear-ASCII data blocks, at offset 192=0xc0.
|
||||
|
// !!!!
|
||||
|
const int DATA_START_OFFSET = 0xc0; |
||||
|
|
||||
|
// The null data block.
|
||||
|
// Length 64=0x40 even if DATA_BLOCK_LENGTH is smaller,
|
||||
|
// to work with 6-bit trail bytes from 2-byte UTF-8.
|
||||
|
const int DATA_NULL_OFFSET = DATA_START_OFFSET; |
||||
|
|
||||
|
// The start of allocated data blocks.
|
||||
|
const int NEW_DATA_START_OFFSET = DATA_NULL_OFFSET + 0x40; |
||||
|
|
||||
|
// The start of data blocks for U+0800 and above.
|
||||
|
// Below, compaction uses a block length of 64 for 2-byte UTF-8.
|
||||
|
// From here on, compaction uses DATA_BLOCK_LENGTH.
|
||||
|
// Data values for 0x780 code points beyond ASCII.
|
||||
|
const int DATA_0800_OFFSET = NEW_DATA_START_OFFSET + 0x780; |
||||
|
|
||||
|
// Start with allocation of 16k data entries. */
|
||||
|
const int INITIAL_DATA_LENGTH = 1 << 14; |
||||
|
|
||||
|
// Grow about 8x each time.
|
||||
|
const int MEDIUM_DATA_LENGTH = 1 << 17; |
||||
|
|
||||
|
// Maximum length of the runtime data array.
|
||||
|
// Limited by 16-bit index values that are left-shifted by INDEX_SHIFT,
|
||||
|
// and by uint16_t UTrie2Header.shiftedDataLength.
|
||||
|
const int MAX_DATA_LENGTH_RUNTIME = 0xffff << INDEX_SHIFT; |
||||
|
|
||||
|
const int INDEX_1_LENGTH = 0x110000 >> SHIFT_1; |
||||
|
|
||||
|
// Maximum length of the build-time data array.
|
||||
|
// One entry per 0x110000 code points, plus the illegal-UTF-8 block and the null block,
|
||||
|
// plus values for the 0x400 surrogate code units.
|
||||
|
const int MAX_DATA_LENGTH_BUILDTIME = 0x110000 + 0x40 + 0x40 + 0x400; |
||||
|
|
||||
|
// At build time, leave a gap in the index-2 table,
|
||||
|
// at least as long as the maximum lengths of the 2-byte UTF-8 index-2 table
|
||||
|
// and the supplementary index-1 table.
|
||||
|
// Round up to INDEX_2_BLOCK_LENGTH for proper compacting.
|
||||
|
const int INDEX_GAP_OFFSET = INDEX_2_BMP_LENGTH; |
||||
|
const int INDEX_GAP_LENGTH = ((UTF8_2B_INDEX_2_LENGTH + MAX_INDEX_1_LENGTH) + INDEX_2_MASK) & ~INDEX_2_MASK; |
||||
|
|
||||
|
// Maximum length of the build-time index-2 array.
|
||||
|
// Maximum number of Unicode code points (0x110000) shifted right by SHIFT_2,
|
||||
|
// plus the part of the index-2 table for lead surrogate code points,
|
||||
|
// plus the build-time index gap,
|
||||
|
// plus the null index-2 block.)
|
||||
|
const int MAX_INDEX_2_LENGTH = (0x110000 >> SHIFT_2) + LSCP_INDEX_2_LENGTH + INDEX_GAP_LENGTH + INDEX_2_BLOCK_LENGTH; |
||||
|
|
||||
|
// The null index-2 block, following the gap in the index-2 table.
|
||||
|
const int INDEX_2_NULL_OFFSET = INDEX_GAP_OFFSET + INDEX_GAP_LENGTH; |
||||
|
|
||||
|
// The start of allocated index-2 blocks.
|
||||
|
const int INDEX_2_START_OFFSET = INDEX_2_NULL_OFFSET + INDEX_2_BLOCK_LENGTH; |
||||
|
|
||||
|
// Maximum length of the runtime index array.
|
||||
|
// Limited by its own 16-bit index values, and by uint16_t UTrie2Header.indexLength.
|
||||
|
// (The actual maximum length is lower,
|
||||
|
// (0x110000>>SHIFT_2)+UTF8_2B_INDEX_2_LENGTH+MAX_INDEX_1_LENGTH.)
|
||||
|
const int MAX_INDEX_LENGTH = 0xffff; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,984 @@ |
|||||
|
// RichTextKit
|
||||
|
// Copyright © 2019 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.
|
||||
|
// Ported from: https://github.com/foliojs/unicode-trie
|
||||
|
// Copied from: https://github.com/toptensoftware/RichTextKit
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting.Unicode |
||||
|
{ |
||||
|
internal partial class UnicodeTrieBuilder |
||||
|
{ |
||||
|
private readonly uint _initialValue; |
||||
|
private readonly uint _errorValue; |
||||
|
private readonly int[] _index1; |
||||
|
private readonly int[] _index2; |
||||
|
private int _highStart; |
||||
|
private uint[] _data; |
||||
|
private int _dataCapacity; |
||||
|
private int _firstFreeBlock; |
||||
|
private bool _isCompacted; |
||||
|
private readonly int[] _map; |
||||
|
private int _dataNullOffset; |
||||
|
private int _dataLength; |
||||
|
private int _index2NullOffset; |
||||
|
private int _index2Length; |
||||
|
|
||||
|
public UnicodeTrieBuilder(uint initialValue = 0, uint errorValue = 0) |
||||
|
{ |
||||
|
_initialValue = initialValue; |
||||
|
_errorValue = errorValue; |
||||
|
_index1 = new int[INDEX_1_LENGTH]; |
||||
|
_index2 = new int[MAX_INDEX_2_LENGTH]; |
||||
|
_highStart = 0x110000; |
||||
|
|
||||
|
_data = new uint[INITIAL_DATA_LENGTH]; |
||||
|
_dataCapacity = INITIAL_DATA_LENGTH; |
||||
|
|
||||
|
_firstFreeBlock = 0; |
||||
|
_isCompacted = false; |
||||
|
|
||||
|
// Multi-purpose per-data-block table.
|
||||
|
//
|
||||
|
// Before compacting:
|
||||
|
//
|
||||
|
// Per-data-block reference counters/free-block list.
|
||||
|
// 0: unused
|
||||
|
// >0: reference counter (number of index-2 entries pointing here)
|
||||
|
// <0: next free data block in free-block list
|
||||
|
//
|
||||
|
// While compacting:
|
||||
|
//
|
||||
|
// Map of adjusted indexes, used in compactData() and compactIndex2().
|
||||
|
// Maps from original indexes to new ones.
|
||||
|
_map = new int[MAX_DATA_LENGTH_BUILDTIME >> SHIFT_2]; |
||||
|
|
||||
|
int i; |
||||
|
for (i = 0; i < 0x80; i++) |
||||
|
{ |
||||
|
_data[i] = _initialValue; |
||||
|
} |
||||
|
|
||||
|
for (; i < 0xc0; i++) |
||||
|
{ |
||||
|
_data[i] = _errorValue; |
||||
|
} |
||||
|
|
||||
|
for (i = DATA_NULL_OFFSET; i < NEW_DATA_START_OFFSET; i++) |
||||
|
{ |
||||
|
_data[i] = _initialValue; |
||||
|
} |
||||
|
|
||||
|
_dataNullOffset = DATA_NULL_OFFSET; |
||||
|
_dataLength = NEW_DATA_START_OFFSET; |
||||
|
|
||||
|
// set the index-2 indexes for the 2=0x80>>SHIFT_2 ASCII data blocks
|
||||
|
int j; |
||||
|
i = 0; |
||||
|
for (j = 0; j < 0x80; j += DATA_BLOCK_LENGTH) { |
||||
|
_index2[i] = j; |
||||
|
_map[i++] = 1; |
||||
|
} |
||||
|
|
||||
|
// reference counts for the bad-UTF-8-data block
|
||||
|
for (; j < 0xc0; j += DATA_BLOCK_LENGTH) { |
||||
|
_map[i++] = 0; |
||||
|
} |
||||
|
|
||||
|
// Reference counts for the null data block: all blocks except for the ASCII blocks.
|
||||
|
// Plus 1 so that we don't drop this block during compaction.
|
||||
|
// Plus as many as needed for lead surrogate code points.
|
||||
|
// i==newTrie->dataNullOffset
|
||||
|
_map[i++] = ((0x110000 >> SHIFT_2) - (0x80 >> SHIFT_2)) + 1 + LSCP_INDEX_2_LENGTH; |
||||
|
j += DATA_BLOCK_LENGTH; |
||||
|
for (; j < NEW_DATA_START_OFFSET; j += DATA_BLOCK_LENGTH) { |
||||
|
_map[i++] = 0; |
||||
|
} |
||||
|
|
||||
|
// set the remaining indexes in the BMP index-2 block
|
||||
|
// to the null data block
|
||||
|
for (i = 0x80 >> SHIFT_2; i < INDEX_2_BMP_LENGTH; i++) { |
||||
|
_index2[i] = DATA_NULL_OFFSET; |
||||
|
} |
||||
|
|
||||
|
// Fill the index gap with impossible values so that compaction
|
||||
|
// does not overlap other index-2 blocks with the gap.
|
||||
|
for (i = 0; i < INDEX_GAP_LENGTH; i++) { |
||||
|
_index2[INDEX_GAP_OFFSET + i] = -1; |
||||
|
} |
||||
|
|
||||
|
// set the indexes in the null index-2 block
|
||||
|
for (i = 0; i < INDEX_2_BLOCK_LENGTH; i++) { |
||||
|
_index2[INDEX_2_NULL_OFFSET + i] = DATA_NULL_OFFSET; |
||||
|
} |
||||
|
|
||||
|
_index2NullOffset = INDEX_2_NULL_OFFSET; |
||||
|
_index2Length = INDEX_2_START_OFFSET; |
||||
|
|
||||
|
// set the index-1 indexes for the linear index-2 block
|
||||
|
j = 0; |
||||
|
for (i = 0; i < OMITTED_BMP_INDEX_1_LENGTH; i++) { |
||||
|
_index1[i] = j; |
||||
|
j += INDEX_2_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
// set the remaining index-1 indexes to the null index-2 block
|
||||
|
for (; i < INDEX_1_LENGTH; i++) { |
||||
|
_index1[i] = INDEX_2_NULL_OFFSET; |
||||
|
} |
||||
|
|
||||
|
// Preallocate and reset data for U+0080..U+07ff,
|
||||
|
// for 2-byte UTF-8 which will be compacted in 64-blocks
|
||||
|
// even if DATA_BLOCK_LENGTH is smaller.
|
||||
|
for (i = 0x80; i < 0x800; i += DATA_BLOCK_LENGTH) { |
||||
|
Set(i, _initialValue); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public UnicodeTrieBuilder Set(int codePoint, uint value) |
||||
|
{ |
||||
|
if ((codePoint < 0) || (codePoint > 0x10ffff)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Invalid code point"); |
||||
|
} |
||||
|
|
||||
|
if (_isCompacted) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Already compacted"); |
||||
|
} |
||||
|
|
||||
|
var block = GetDataBlock(codePoint, true); |
||||
|
_data[block + (codePoint & DATA_MASK)] = value; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public UnicodeTrieBuilder SetRange(int start, int end, uint value, bool overwrite = true) |
||||
|
{ |
||||
|
|
||||
|
if ((start > 0x10ffff) || (end > 0x10ffff) || (start > end)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Invalid code point"); |
||||
|
} |
||||
|
|
||||
|
if (_isCompacted) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Already compacted"); |
||||
|
} |
||||
|
|
||||
|
if (!overwrite && (value == _initialValue)) |
||||
|
{ |
||||
|
return this; // nothing to do
|
||||
|
} |
||||
|
|
||||
|
var limit = end + 1; |
||||
|
if ((start & DATA_MASK) != 0) |
||||
|
{ |
||||
|
// set partial block at [start..following block boundary
|
||||
|
var block = GetDataBlock(start, true); |
||||
|
|
||||
|
var nextStart = (start + DATA_BLOCK_LENGTH) & ~DATA_MASK; |
||||
|
if (nextStart <= limit) |
||||
|
{ |
||||
|
FillBlock(block, start & DATA_MASK, DATA_BLOCK_LENGTH, value, _initialValue, overwrite); |
||||
|
start = nextStart; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
FillBlock(block, start & DATA_MASK, limit & DATA_MASK, value, _initialValue, overwrite); |
||||
|
return this; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// number of positions in the last, partial block
|
||||
|
var rest = limit & DATA_MASK; |
||||
|
|
||||
|
// round down limit to a block boundary
|
||||
|
limit &= ~DATA_MASK; |
||||
|
|
||||
|
// iterate over all-value blocks
|
||||
|
int repeatBlock; |
||||
|
if (value == _initialValue) |
||||
|
{ |
||||
|
repeatBlock = _dataNullOffset; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
repeatBlock = -1; |
||||
|
} |
||||
|
|
||||
|
while (start < limit) |
||||
|
{ |
||||
|
var setRepeatBlock = false; |
||||
|
|
||||
|
if ((value == _initialValue) && IsInNullBlock(start, true)) |
||||
|
{ |
||||
|
start += DATA_BLOCK_LENGTH; // nothing to do
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// get index value
|
||||
|
var i2 = GetIndex2Block(start, true); |
||||
|
i2 += (start >> SHIFT_2) & INDEX_2_MASK; |
||||
|
|
||||
|
var block = _index2[i2]; |
||||
|
if (IsWritableBlock(block)) |
||||
|
{ |
||||
|
// already allocated
|
||||
|
if (overwrite && (block >= DATA_0800_OFFSET)) |
||||
|
{ |
||||
|
// We overwrite all values, and it's not a
|
||||
|
// protected (ASCII-linear or 2-byte UTF-8) block:
|
||||
|
// replace with the repeatBlock.
|
||||
|
setRepeatBlock = true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// protected block: just write the values into this block
|
||||
|
FillBlock(block, 0, DATA_BLOCK_LENGTH, value, _initialValue, overwrite); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
else if ((_data[block] != value) && (overwrite || (block == _dataNullOffset))) |
||||
|
{ |
||||
|
// Set the repeatBlock instead of the null block or previous repeat block:
|
||||
|
//
|
||||
|
// If !isWritableBlock() then all entries in the block have the same value
|
||||
|
// because it's the null block or a range block (the repeatBlock from a previous
|
||||
|
// call to utrie2_setRange32()).
|
||||
|
// No other blocks are used multiple times before compacting.
|
||||
|
//
|
||||
|
// The null block is the only non-writable block with the initialValue because
|
||||
|
// of the repeatBlock initialization above. (If value==initialValue, then
|
||||
|
// the repeatBlock will be the null data block.)
|
||||
|
//
|
||||
|
// We set our repeatBlock if the desired value differs from the block's value,
|
||||
|
// and if we overwrite any data or if the data is all initial values
|
||||
|
// (which is the same as the block being the null block, see above).
|
||||
|
setRepeatBlock = true; |
||||
|
} |
||||
|
|
||||
|
if (setRepeatBlock) |
||||
|
{ |
||||
|
if (repeatBlock >= 0) |
||||
|
{ |
||||
|
SetIndex2Entry(i2, repeatBlock); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// create and set and fill the repeatBlock
|
||||
|
repeatBlock = GetDataBlock(start, true); |
||||
|
WriteBlock(repeatBlock, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
start += DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
if (rest > 0) |
||||
|
{ |
||||
|
// set partial block at [last block boundary..limit
|
||||
|
var block = GetDataBlock(start, true); |
||||
|
FillBlock(block, 0, rest, value, _initialValue, overwrite); |
||||
|
} |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public uint Get(int c, bool fromLSCP = true) |
||||
|
{ |
||||
|
if ((c < 0) || (c > 0x10ffff)) |
||||
|
{ |
||||
|
return _errorValue; |
||||
|
} |
||||
|
|
||||
|
if ((c >= _highStart) && (!((c >= 0xd800) && (c < 0xdc00)) || fromLSCP)) |
||||
|
{ |
||||
|
return _data[_dataLength - DATA_GRANULARITY]; |
||||
|
} |
||||
|
|
||||
|
int i2; |
||||
|
if (((c >= 0xd800) && (c < 0xdc00)) && fromLSCP) |
||||
|
{ |
||||
|
i2 = (LSCP_INDEX_2_OFFSET - (0xd800 >> SHIFT_2)) + (c >> SHIFT_2); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
i2 = _index1[c >> SHIFT_1] + ((c >> SHIFT_2) & INDEX_2_MASK); |
||||
|
} |
||||
|
|
||||
|
var block = _index2[i2]; |
||||
|
return _data[block + (c & DATA_MASK)]; |
||||
|
} |
||||
|
|
||||
|
public byte[] ToBuffer() |
||||
|
{ |
||||
|
var mem = new MemoryStream(); |
||||
|
Save(mem); |
||||
|
return mem.GetBuffer(); |
||||
|
} |
||||
|
|
||||
|
public void Save(Stream stream) |
||||
|
{ |
||||
|
var trie = this.Freeze(); |
||||
|
trie.Save(stream); |
||||
|
} |
||||
|
|
||||
|
public UnicodeTrie Freeze() |
||||
|
{ |
||||
|
int allIndexesLength, i; |
||||
|
if (!_isCompacted) |
||||
|
{ |
||||
|
Compact(); |
||||
|
} |
||||
|
|
||||
|
if (_highStart <= 0x10000) |
||||
|
{ |
||||
|
allIndexesLength = INDEX_1_OFFSET; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
allIndexesLength = _index2Length; |
||||
|
} |
||||
|
|
||||
|
var dataMove = allIndexesLength; |
||||
|
|
||||
|
// are indexLength and dataLength within limits?
|
||||
|
if ((allIndexesLength > MAX_INDEX_LENGTH) || // for unshifted indexLength
|
||||
|
((dataMove + _dataNullOffset) > 0xffff) || // for unshifted dataNullOffset
|
||||
|
((dataMove + DATA_0800_OFFSET) > 0xffff) || // for unshifted 2-byte UTF-8 index-2 values
|
||||
|
((dataMove + _dataLength) > MAX_DATA_LENGTH_RUNTIME)) |
||||
|
{ // for shiftedDataLength
|
||||
|
throw new InvalidOperationException("Trie data is too large."); |
||||
|
} |
||||
|
|
||||
|
// calculate the sizes of, and allocate, the index and data arrays
|
||||
|
var indexLength = allIndexesLength + _dataLength; |
||||
|
var data = new int[indexLength]; |
||||
|
|
||||
|
// write the index-2 array values shifted right by INDEX_SHIFT, after adding dataMove
|
||||
|
var destIdx = 0; |
||||
|
for (i = 0; i < INDEX_2_BMP_LENGTH; i++) |
||||
|
{ |
||||
|
data[destIdx++] = ((_index2[i] + dataMove) >> INDEX_SHIFT); |
||||
|
} |
||||
|
|
||||
|
// write UTF-8 2-byte index-2 values, not right-shifted
|
||||
|
for (i = 0; i < 0xc2 - 0xc0; i++) |
||||
|
{ // C0..C1
|
||||
|
data[destIdx++] = (dataMove + BAD_UTF8_DATA_OFFSET); |
||||
|
} |
||||
|
|
||||
|
for (; i < 0xe0 - 0xc0; i++) |
||||
|
{ // C2..DF
|
||||
|
data[destIdx++] = (dataMove + _index2[i << (6 - SHIFT_2)]); |
||||
|
} |
||||
|
|
||||
|
if (_highStart > 0x10000) |
||||
|
{ |
||||
|
var index1Length = (_highStart - 0x10000) >> SHIFT_1; |
||||
|
var index2Offset = INDEX_2_BMP_LENGTH + UTF8_2B_INDEX_2_LENGTH + index1Length; |
||||
|
|
||||
|
// write 16-bit index-1 values for supplementary code points
|
||||
|
for (i = 0; i < index1Length; i++) |
||||
|
{ |
||||
|
data[destIdx++] = (INDEX_2_OFFSET + _index1[i + OMITTED_BMP_INDEX_1_LENGTH]); |
||||
|
} |
||||
|
|
||||
|
// write the index-2 array values for supplementary code points,
|
||||
|
// shifted right by INDEX_SHIFT, after adding dataMove
|
||||
|
for (i = 0; i < _index2Length - index2Offset; i++) |
||||
|
{ |
||||
|
data[destIdx++] = ((dataMove + _index2[index2Offset + i]) >> INDEX_SHIFT); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// write 16-bit data values
|
||||
|
for (i = 0; i < _dataLength; i++) |
||||
|
{ |
||||
|
data[destIdx++] = (int)_data[i]; |
||||
|
} |
||||
|
|
||||
|
return new UnicodeTrie(data, _highStart, _errorValue); |
||||
|
} |
||||
|
|
||||
|
private bool IsInNullBlock(int c, bool forLSCP) |
||||
|
{ |
||||
|
int i2; |
||||
|
if (((c & 0xfffffc00) == 0xd800) && forLSCP) |
||||
|
{ |
||||
|
i2 = (LSCP_INDEX_2_OFFSET - (0xd800 >> SHIFT_2)) + (c >> SHIFT_2); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
i2 = _index1[c >> SHIFT_1] + ((c >> SHIFT_2) & INDEX_2_MASK); |
||||
|
} |
||||
|
|
||||
|
var block = _index2[i2]; |
||||
|
return block == _dataNullOffset; |
||||
|
} |
||||
|
|
||||
|
private int AllocIndex2Block() |
||||
|
{ |
||||
|
var newBlock = _index2Length; |
||||
|
var newTop = newBlock + INDEX_2_BLOCK_LENGTH; |
||||
|
if (newTop > _index2.Length) |
||||
|
{ |
||||
|
// Should never occur.
|
||||
|
// Either MAX_BUILD_TIME_INDEX_LENGTH is incorrect,
|
||||
|
// or the code writes more values than should be possible.
|
||||
|
throw new InvalidOperationException("Internal error in Trie2 creation."); |
||||
|
} |
||||
|
|
||||
|
_index2Length = newTop; |
||||
|
Array.Copy(_index2, _index2NullOffset, _index2, newBlock, INDEX_2_BLOCK_LENGTH); |
||||
|
|
||||
|
return newBlock; |
||||
|
} |
||||
|
|
||||
|
private int GetIndex2Block(int c, bool forLSCP) |
||||
|
{ |
||||
|
if ((c >= 0xd800) && (c < 0xdc00) && forLSCP) |
||||
|
{ |
||||
|
return LSCP_INDEX_2_OFFSET; |
||||
|
} |
||||
|
|
||||
|
var i1 = c >> SHIFT_1; |
||||
|
var i2 = _index1[i1]; |
||||
|
if (i2 == _index2NullOffset) |
||||
|
{ |
||||
|
i2 = AllocIndex2Block(); |
||||
|
_index1[i1] = i2; |
||||
|
} |
||||
|
|
||||
|
return i2; |
||||
|
} |
||||
|
|
||||
|
private bool IsWritableBlock(int block) |
||||
|
{ |
||||
|
return (block != _dataNullOffset) && (_map[block >> SHIFT_2] == 1); |
||||
|
} |
||||
|
|
||||
|
private int AllocDataBlock(int copyBlock) |
||||
|
{ |
||||
|
int newBlock; |
||||
|
if (_firstFreeBlock != 0) |
||||
|
{ |
||||
|
// get the first free block
|
||||
|
newBlock = _firstFreeBlock; |
||||
|
_firstFreeBlock = -_map[newBlock >> SHIFT_2]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// get a new block from the high end
|
||||
|
newBlock = _dataLength; |
||||
|
var newTop = newBlock + DATA_BLOCK_LENGTH; |
||||
|
if (newTop > _dataCapacity) |
||||
|
{ |
||||
|
// out of memory in the data array
|
||||
|
int capacity; |
||||
|
if (_dataCapacity < MEDIUM_DATA_LENGTH) |
||||
|
{ |
||||
|
capacity = MEDIUM_DATA_LENGTH; |
||||
|
} |
||||
|
else if (_dataCapacity < MAX_DATA_LENGTH_BUILDTIME) |
||||
|
{ |
||||
|
capacity = MAX_DATA_LENGTH_BUILDTIME; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Should never occur.
|
||||
|
// Either MAX_DATA_LENGTH_BUILDTIME is incorrect,
|
||||
|
// or the code writes more values than should be possible.
|
||||
|
throw new InvalidOperationException("Internal error in Trie2 creation."); |
||||
|
} |
||||
|
|
||||
|
var newData = new UInt32[capacity]; |
||||
|
Array.Copy(_data, newData, _dataLength); |
||||
|
_data = newData; |
||||
|
_dataCapacity = capacity; |
||||
|
} |
||||
|
|
||||
|
_dataLength = newTop; |
||||
|
} |
||||
|
|
||||
|
Array.Copy(_data, copyBlock, _data, newBlock, DATA_BLOCK_LENGTH); |
||||
|
//_data.set(_data.subarray(copyBlock, copyBlock + DATA_BLOCK_LENGTH), newBlock);
|
||||
|
_map[newBlock >> SHIFT_2] = 0; |
||||
|
return newBlock; |
||||
|
} |
||||
|
|
||||
|
private void ReleaseDataBlock(int block) |
||||
|
{ |
||||
|
// put this block at the front of the free-block chain
|
||||
|
_map[block >> SHIFT_2] = -_firstFreeBlock; |
||||
|
_firstFreeBlock = block; |
||||
|
} |
||||
|
|
||||
|
private void SetIndex2Entry(int i2, int block) |
||||
|
{ |
||||
|
++_map[block >> SHIFT_2]; // increment first, in case block == oldBlock!
|
||||
|
var oldBlock = _index2[i2]; |
||||
|
if (--_map[oldBlock >> SHIFT_2] == 0) |
||||
|
{ |
||||
|
ReleaseDataBlock(oldBlock); |
||||
|
} |
||||
|
|
||||
|
_index2[i2] = block; |
||||
|
} |
||||
|
|
||||
|
private int GetDataBlock(int c, bool forLSCP) |
||||
|
{ |
||||
|
var i2 = GetIndex2Block(c, forLSCP); |
||||
|
i2 += (c >> SHIFT_2) & INDEX_2_MASK; |
||||
|
|
||||
|
var oldBlock = _index2[i2]; |
||||
|
if (IsWritableBlock(oldBlock)) |
||||
|
{ |
||||
|
return oldBlock; |
||||
|
} |
||||
|
|
||||
|
// allocate a new data block
|
||||
|
var newBlock = AllocDataBlock(oldBlock); |
||||
|
SetIndex2Entry(i2, newBlock); |
||||
|
return newBlock; |
||||
|
} |
||||
|
|
||||
|
private void FillBlock(int block, int start, int limit, uint value, uint initialValue, bool overwrite) |
||||
|
{ |
||||
|
int i; |
||||
|
if (overwrite) |
||||
|
{ |
||||
|
for (i = block + start; i < block + limit; i++) |
||||
|
{ |
||||
|
_data[i] = value; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
for (i = block + start; i < block + limit; i++) |
||||
|
{ |
||||
|
if (_data[i] == initialValue) |
||||
|
{ |
||||
|
_data[i] = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void WriteBlock(int block, uint value) |
||||
|
{ |
||||
|
var limit = block + DATA_BLOCK_LENGTH; |
||||
|
while (block < limit) |
||||
|
{ |
||||
|
_data[block++] = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int FindHighStart(uint highValue) |
||||
|
{ |
||||
|
int prevBlock, prevI2Block; |
||||
|
|
||||
|
// set variables for previous range
|
||||
|
if (highValue == _initialValue) |
||||
|
{ |
||||
|
prevI2Block = _index2NullOffset; |
||||
|
prevBlock = _dataNullOffset; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
prevI2Block = -1; |
||||
|
prevBlock = -1; |
||||
|
} |
||||
|
|
||||
|
int prev = 0x110000; |
||||
|
|
||||
|
// enumerate index-2 blocks
|
||||
|
var i1 = INDEX_1_LENGTH; |
||||
|
var c = prev; |
||||
|
while (c > 0) |
||||
|
{ |
||||
|
var i2Block = _index1[--i1]; |
||||
|
if (i2Block == prevI2Block) |
||||
|
{ |
||||
|
// the index-2 block is the same as the previous one, and filled with highValue
|
||||
|
c -= CP_PER_INDEX_1_ENTRY; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
prevI2Block = i2Block; |
||||
|
if (i2Block == _index2NullOffset) |
||||
|
{ |
||||
|
// this is the null index-2 block
|
||||
|
if (highValue != _initialValue) |
||||
|
{ |
||||
|
return c; |
||||
|
} |
||||
|
c -= CP_PER_INDEX_1_ENTRY; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// enumerate data blocks for one index-2 block
|
||||
|
var i2 = INDEX_2_BLOCK_LENGTH; |
||||
|
while (i2 > 0) |
||||
|
{ |
||||
|
var block = _index2[i2Block + --i2]; |
||||
|
if (block == prevBlock) |
||||
|
{ |
||||
|
// the block is the same as the previous one, and filled with highValue
|
||||
|
c -= DATA_BLOCK_LENGTH; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
prevBlock = block; |
||||
|
if (block == _dataNullOffset) |
||||
|
{ |
||||
|
// this is the null data block
|
||||
|
if (highValue != _initialValue) |
||||
|
{ |
||||
|
return c; |
||||
|
} |
||||
|
c -= DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var j = DATA_BLOCK_LENGTH; |
||||
|
while (j > 0) |
||||
|
{ |
||||
|
var value = _data[block + --j]; |
||||
|
if (value != highValue) |
||||
|
{ |
||||
|
return c; |
||||
|
} |
||||
|
--c; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// deliver last range
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
private int FindSameDataBlock(int dataLength, int otherBlock, int blockLength) |
||||
|
{ |
||||
|
// ensure that we do not even partially get past dataLength
|
||||
|
dataLength -= blockLength; |
||||
|
var block = 0; |
||||
|
while (block <= dataLength) |
||||
|
{ |
||||
|
if (EqualSequence(_data, block, otherBlock, blockLength)) |
||||
|
{ |
||||
|
return block; |
||||
|
} |
||||
|
block += DATA_GRANULARITY; |
||||
|
} |
||||
|
|
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
private int FindSameIndex2Block(int index2Length, int otherBlock) { |
||||
|
// ensure that we do not even partially get past index2Length
|
||||
|
index2Length -= INDEX_2_BLOCK_LENGTH; |
||||
|
for (var block = 0; block <= index2Length; block++) |
||||
|
{ |
||||
|
if (EqualSequence(_index2, block, otherBlock, INDEX_2_BLOCK_LENGTH)) |
||||
|
{ |
||||
|
return block; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
private void CompactData() |
||||
|
{ |
||||
|
// do not compact linear-ASCII data
|
||||
|
var newStart = DATA_START_OFFSET; |
||||
|
var start = 0; |
||||
|
var i = 0; |
||||
|
|
||||
|
while (start < newStart) |
||||
|
{ |
||||
|
_map[i++] = start; |
||||
|
start += DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
// Start with a block length of 64 for 2-byte UTF-8,
|
||||
|
// then switch to DATA_BLOCK_LENGTH.
|
||||
|
var blockLength = 64; |
||||
|
var blockCount = blockLength >> SHIFT_2; |
||||
|
start = newStart; |
||||
|
while (start < _dataLength) |
||||
|
{ |
||||
|
// start: index of first entry of current block
|
||||
|
// newStart: index where the current block is to be moved
|
||||
|
// (right after current end of already-compacted data)
|
||||
|
int mapIndex, movedStart; |
||||
|
if (start == DATA_0800_OFFSET) |
||||
|
{ |
||||
|
blockLength = DATA_BLOCK_LENGTH; |
||||
|
blockCount = 1; |
||||
|
} |
||||
|
|
||||
|
// skip blocks that are not used
|
||||
|
if (_map[start >> SHIFT_2] <= 0) |
||||
|
{ |
||||
|
// advance start to the next block
|
||||
|
start += blockLength; |
||||
|
|
||||
|
// leave newStart with the previous block!
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// search for an identical block
|
||||
|
if ((movedStart = FindSameDataBlock(newStart, start, blockLength)) >= 0) |
||||
|
{ |
||||
|
// found an identical block, set the other block's index value for the current block
|
||||
|
mapIndex = start >> SHIFT_2; |
||||
|
for (i = blockCount; i > 0; i--) |
||||
|
{ |
||||
|
_map[mapIndex++] = movedStart; |
||||
|
movedStart += DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
// advance start to the next block
|
||||
|
start += blockLength; |
||||
|
|
||||
|
// leave newStart with the previous block!
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// see if the beginning of this block can be overlapped with the end of the previous block
|
||||
|
// look for maximum overlap (modulo granularity) with the previous, adjacent block
|
||||
|
var overlap = blockLength - DATA_GRANULARITY; |
||||
|
while ((overlap > 0) && !EqualSequence(_data, (newStart - overlap), start, overlap)) |
||||
|
{ |
||||
|
overlap -= DATA_GRANULARITY; |
||||
|
} |
||||
|
|
||||
|
if ((overlap > 0) || (newStart < start)) |
||||
|
{ |
||||
|
// some overlap, or just move the whole block
|
||||
|
movedStart = newStart - overlap; |
||||
|
mapIndex = start >> SHIFT_2; |
||||
|
|
||||
|
for (i = blockCount; i > 0; i--) |
||||
|
{ |
||||
|
_map[mapIndex++] = movedStart; |
||||
|
movedStart += DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
// move the non-overlapping indexes to their new positions
|
||||
|
start += overlap; |
||||
|
for (i = blockLength - overlap; i > 0; i--) |
||||
|
{ |
||||
|
_data[newStart++] = _data[start++]; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
else |
||||
|
{ // no overlap && newStart==start
|
||||
|
mapIndex = start >> SHIFT_2; |
||||
|
for (i = blockCount; i > 0; i--) |
||||
|
{ |
||||
|
_map[mapIndex++] = start; |
||||
|
start += DATA_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
newStart = start; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// now adjust the index-2 table
|
||||
|
i = 0; |
||||
|
while (i < _index2Length) |
||||
|
{ |
||||
|
// Gap indexes are invalid (-1). Skip over the gap.
|
||||
|
if (i == INDEX_GAP_OFFSET) |
||||
|
{ |
||||
|
i += INDEX_GAP_LENGTH; |
||||
|
} |
||||
|
_index2[i] = _map[_index2[i] >> SHIFT_2]; |
||||
|
++i; |
||||
|
} |
||||
|
|
||||
|
_dataNullOffset = _map[_dataNullOffset >> SHIFT_2]; |
||||
|
|
||||
|
// ensure dataLength alignment
|
||||
|
while ((newStart & (DATA_GRANULARITY - 1)) != 0) |
||||
|
{ |
||||
|
_data[newStart++] = _initialValue; |
||||
|
} |
||||
|
_dataLength = newStart; |
||||
|
} |
||||
|
|
||||
|
private void CompactIndex2() |
||||
|
{ |
||||
|
// do not compact linear-BMP index-2 blocks
|
||||
|
var newStart = INDEX_2_BMP_LENGTH; |
||||
|
var start = 0; |
||||
|
var i = 0; |
||||
|
|
||||
|
while (start < newStart) |
||||
|
{ |
||||
|
_map[i++] = start; |
||||
|
start += INDEX_2_BLOCK_LENGTH; |
||||
|
} |
||||
|
|
||||
|
// Reduce the index table gap to what will be needed at runtime.
|
||||
|
newStart += UTF8_2B_INDEX_2_LENGTH + ((_highStart - 0x10000) >> SHIFT_1); |
||||
|
start = INDEX_2_NULL_OFFSET; |
||||
|
while (start < _index2Length) |
||||
|
{ |
||||
|
// start: index of first entry of current block
|
||||
|
// newStart: index where the current block is to be moved
|
||||
|
// (right after current end of already-compacted data)
|
||||
|
|
||||
|
// search for an identical block
|
||||
|
int movedStart; |
||||
|
if ((movedStart = FindSameIndex2Block(newStart, start)) >= 0) |
||||
|
{ |
||||
|
// found an identical block, set the other block's index value for the current block
|
||||
|
_map[start >> SHIFT_1_2] = movedStart; |
||||
|
|
||||
|
// advance start to the next block
|
||||
|
start += INDEX_2_BLOCK_LENGTH; |
||||
|
|
||||
|
// leave newStart with the previous block!
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// see if the beginning of this block can be overlapped with the end of the previous block
|
||||
|
// look for maximum overlap with the previous, adjacent block
|
||||
|
var overlap = INDEX_2_BLOCK_LENGTH - 1; |
||||
|
while ((overlap > 0) && !EqualSequence(_index2, (newStart - overlap), start, overlap)) |
||||
|
{ |
||||
|
--overlap; |
||||
|
} |
||||
|
|
||||
|
if ((overlap > 0) || (newStart < start)) |
||||
|
{ |
||||
|
// some overlap, or just move the whole block
|
||||
|
_map[start >> SHIFT_1_2] = newStart - overlap; |
||||
|
|
||||
|
// move the non-overlapping indexes to their new positions
|
||||
|
start += overlap; |
||||
|
for (i = INDEX_2_BLOCK_LENGTH - overlap; i > 0; i--) |
||||
|
{ |
||||
|
_index2[newStart++] = _index2[start++]; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
else |
||||
|
{ // no overlap && newStart==start
|
||||
|
_map[start >> SHIFT_1_2] = start; |
||||
|
start += INDEX_2_BLOCK_LENGTH; |
||||
|
newStart = start; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// now adjust the index-1 table
|
||||
|
for (i = 0; i < INDEX_1_LENGTH; i++) |
||||
|
{ |
||||
|
_index1[i] = _map[_index1[i] >> SHIFT_1_2]; |
||||
|
} |
||||
|
|
||||
|
_index2NullOffset = _map[_index2NullOffset >> SHIFT_1_2]; |
||||
|
|
||||
|
// Ensure data table alignment:
|
||||
|
// Needs to be granularity-aligned for 16-bit trie
|
||||
|
// (so that dataMove will be down-shiftable),
|
||||
|
// and 2-aligned for uint32_t data.
|
||||
|
|
||||
|
// Arbitrary value: 0x3fffc not possible for real data.
|
||||
|
while ((newStart & ((DATA_GRANULARITY - 1) | 1)) != 0) |
||||
|
{ |
||||
|
_index2[newStart++] = 0x0000ffff << INDEX_SHIFT; |
||||
|
} |
||||
|
|
||||
|
_index2Length = newStart; |
||||
|
} |
||||
|
|
||||
|
private void Compact() |
||||
|
{ |
||||
|
// find highStart and round it up
|
||||
|
var highValue = Get(0x10ffff); |
||||
|
var highStart = FindHighStart(highValue); |
||||
|
highStart = (highStart + (CP_PER_INDEX_1_ENTRY - 1)) & ~(CP_PER_INDEX_1_ENTRY - 1); |
||||
|
if (highStart == 0x110000) |
||||
|
{ |
||||
|
highValue = _errorValue; |
||||
|
} |
||||
|
|
||||
|
// Set trie->highStart only after utrie2_get32(trie, highStart).
|
||||
|
// Otherwise utrie2_get32(trie, highStart) would try to read the highValue.
|
||||
|
_highStart = highStart; |
||||
|
if (_highStart < 0x110000) |
||||
|
{ |
||||
|
// Blank out [highStart..10ffff] to release associated data blocks.
|
||||
|
var suppHighStart = _highStart <= 0x10000 ? 0x10000 : _highStart; |
||||
|
SetRange(suppHighStart, 0x10ffff, _initialValue); |
||||
|
} |
||||
|
|
||||
|
CompactData(); |
||||
|
|
||||
|
if (_highStart > 0x10000) |
||||
|
{ |
||||
|
CompactIndex2(); |
||||
|
} |
||||
|
|
||||
|
// Store the highValue in the data array and round up the dataLength.
|
||||
|
// Must be done after compactData() because that assumes that dataLength
|
||||
|
// is a multiple of DATA_BLOCK_LENGTH.
|
||||
|
_data[_dataLength++] = highValue; |
||||
|
while ((_dataLength & (DATA_GRANULARITY - 1)) != 0) |
||||
|
{ |
||||
|
_data[_dataLength++] = _initialValue; |
||||
|
} |
||||
|
|
||||
|
_isCompacted = true; |
||||
|
} |
||||
|
|
||||
|
private static bool EqualSequence(IReadOnlyList<uint> a, int s, int t, int length) |
||||
|
{ |
||||
|
for (var i = 0; i < length; i++) |
||||
|
{ |
||||
|
if (a[s + i] != a[t + i]) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static bool EqualSequence(IReadOnlyList<int> a, int s, int t, int length) |
||||
|
{ |
||||
|
for (var i = 0; i < length; i++) |
||||
|
{ |
||||
|
if (a[s + i] != a[t + i]) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Describes how text is trimmed when it overflows.
|
||||
|
/// </summary>
|
||||
|
public enum TextTrimming |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Text is not trimmed.
|
||||
|
/// </summary>
|
||||
|
None, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
|
/// </summary>
|
||||
|
CharacterEllipsis, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
|
/// </summary>
|
||||
|
WordEllipsis |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.Platform |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An abstraction that is used produce shaped text.
|
||||
|
/// </summary>
|
||||
|
public interface ITextShaperImpl |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Shapes the specified region within the text and returns a resulting glyph run.
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">The text.</param>
|
||||
|
/// <param name="textFormat">The text format.</param>
|
||||
|
/// <returns>A shaped glyph run.</returns>
|
||||
|
GlyphRun ShapeText(ReadOnlySlice<char> text, TextFormat textFormat); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
using HarfBuzzSharp; |
||||
|
using Buffer = HarfBuzzSharp.Buffer; |
||||
|
|
||||
|
namespace Avalonia.Skia |
||||
|
{ |
||||
|
internal class TextShaperImpl : ITextShaperImpl |
||||
|
{ |
||||
|
public GlyphRun ShapeText(ReadOnlySlice<char> text, TextFormat textFormat) |
||||
|
{ |
||||
|
using (var buffer = new Buffer()) |
||||
|
{ |
||||
|
buffer.ContentType = ContentType.Unicode; |
||||
|
|
||||
|
var breakCharPosition = text.Length - 1; |
||||
|
|
||||
|
var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); |
||||
|
|
||||
|
if (codepoint.IsBreakChar) |
||||
|
{ |
||||
|
var breakCharCount = 1; |
||||
|
|
||||
|
if (text.Length > 1) |
||||
|
{ |
||||
|
var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); |
||||
|
|
||||
|
if (codepoint == '\r' && previousCodepoint == '\n' |
||||
|
|| codepoint == '\n' && previousCodepoint == '\r') |
||||
|
{ |
||||
|
breakCharCount = 2; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (breakCharPosition != text.Start) |
||||
|
{ |
||||
|
buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); |
||||
|
} |
||||
|
|
||||
|
var cluster = buffer.GlyphInfos.Length > 0 ? |
||||
|
buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : |
||||
|
(uint)text.Start; |
||||
|
|
||||
|
switch (breakCharCount) |
||||
|
{ |
||||
|
case 1: |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
break; |
||||
|
case 2: |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
buffer.Add('\u200D', cluster); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
buffer.AddUtf16(text.Buffer.Span); |
||||
|
} |
||||
|
|
||||
|
buffer.GuessSegmentProperties(); |
||||
|
|
||||
|
var glyphTypeface = textFormat.Typeface.GlyphTypeface; |
||||
|
|
||||
|
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; |
||||
|
|
||||
|
font.Shape(buffer); |
||||
|
|
||||
|
font.GetScale(out var scaleX, out _); |
||||
|
|
||||
|
var textScale = textFormat.FontRenderingEmSize / scaleX; |
||||
|
|
||||
|
var len = buffer.Length; |
||||
|
|
||||
|
var info = buffer.GetGlyphInfoSpan(); |
||||
|
|
||||
|
var pos = buffer.GetGlyphPositionSpan(); |
||||
|
|
||||
|
var glyphIndices = new ushort[len]; |
||||
|
|
||||
|
var clusters = new ushort[len]; |
||||
|
|
||||
|
var glyphAdvances = new double[len]; |
||||
|
|
||||
|
var glyphOffsets = new Vector[len]; |
||||
|
|
||||
|
for (var i = 0; i < len; i++) |
||||
|
{ |
||||
|
glyphIndices[i] = (ushort)info[i].Codepoint; |
||||
|
|
||||
|
clusters[i] = (ushort)(text.Start + info[i].Cluster); |
||||
|
|
||||
|
var advanceX = pos[i].XAdvance * textScale; |
||||
|
// Depends on direction of layout
|
||||
|
//var advanceY = pos[i].YAdvance * textScale;
|
||||
|
|
||||
|
glyphAdvances[i] = advanceX; |
||||
|
|
||||
|
var offsetX = pos[i].XOffset * textScale; |
||||
|
var offsetY = pos[i].YOffset * textScale; |
||||
|
|
||||
|
glyphOffsets[i] = new Vector(offsetX, offsetY); |
||||
|
} |
||||
|
|
||||
|
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, |
||||
|
new ReadOnlySlice<ushort>(glyphIndices), |
||||
|
new ReadOnlySlice<double>(glyphAdvances), |
||||
|
new ReadOnlySlice<Vector>(glyphOffsets), |
||||
|
text, |
||||
|
new ReadOnlySlice<ushort>(clusters)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
using HarfBuzzSharp; |
||||
|
using Buffer = HarfBuzzSharp.Buffer; |
||||
|
|
||||
|
namespace Avalonia.Direct2D1.Media |
||||
|
{ |
||||
|
internal class TextShaperImpl : ITextShaperImpl |
||||
|
{ |
||||
|
public GlyphRun ShapeText(ReadOnlySlice<char> text, TextFormat textFormat) |
||||
|
{ |
||||
|
using (var buffer = new Buffer()) |
||||
|
{ |
||||
|
buffer.ContentType = ContentType.Unicode; |
||||
|
|
||||
|
var breakCharPosition = text.Length - 1; |
||||
|
|
||||
|
var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); |
||||
|
|
||||
|
if (codepoint.IsBreakChar) |
||||
|
{ |
||||
|
var breakCharCount = 1; |
||||
|
|
||||
|
if (text.Length > 1) |
||||
|
{ |
||||
|
var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); |
||||
|
|
||||
|
if (codepoint == '\r' && previousCodepoint == '\n' |
||||
|
|| codepoint == '\n' && previousCodepoint == '\r') |
||||
|
{ |
||||
|
breakCharCount = 2; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (breakCharPosition != text.Start) |
||||
|
{ |
||||
|
buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); |
||||
|
} |
||||
|
|
||||
|
var cluster = buffer.GlyphInfos.Length > 0 ? |
||||
|
buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : |
||||
|
(uint)text.Start; |
||||
|
|
||||
|
switch (breakCharCount) |
||||
|
{ |
||||
|
case 1: |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
break; |
||||
|
case 2: |
||||
|
buffer.Add('\u200C', cluster); |
||||
|
buffer.Add('\u200D', cluster); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
buffer.AddUtf16(text.Buffer.Span); |
||||
|
} |
||||
|
|
||||
|
buffer.GuessSegmentProperties(); |
||||
|
|
||||
|
var glyphTypeface = textFormat.Typeface.GlyphTypeface; |
||||
|
|
||||
|
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; |
||||
|
|
||||
|
font.Shape(buffer); |
||||
|
|
||||
|
font.GetScale(out var scaleX, out _); |
||||
|
|
||||
|
var textScale = textFormat.FontRenderingEmSize / scaleX; |
||||
|
|
||||
|
var len = buffer.Length; |
||||
|
|
||||
|
var info = buffer.GetGlyphInfoSpan(); |
||||
|
|
||||
|
var pos = buffer.GetGlyphPositionSpan(); |
||||
|
|
||||
|
var glyphIndices = new ushort[len]; |
||||
|
|
||||
|
var clusters = new ushort[len]; |
||||
|
|
||||
|
var glyphAdvances = new double[len]; |
||||
|
|
||||
|
var glyphOffsets = new Vector[len]; |
||||
|
|
||||
|
for (var i = 0; i < len; i++) |
||||
|
{ |
||||
|
glyphIndices[i] = (ushort)info[i].Codepoint; |
||||
|
|
||||
|
clusters[i] = (ushort)(text.Start + info[i].Cluster); |
||||
|
|
||||
|
var advanceX = pos[i].XAdvance * textScale; |
||||
|
// Depends on direction of layout
|
||||
|
//var advanceY = pos[i].YAdvance * textScale;
|
||||
|
|
||||
|
glyphAdvances[i] = advanceX; |
||||
|
|
||||
|
var offsetX = pos[i].XOffset * textScale; |
||||
|
var offsetY = pos[i].YOffset * textScale; |
||||
|
|
||||
|
glyphOffsets[i] = new Vector(offsetX, offsetY); |
||||
|
} |
||||
|
|
||||
|
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, |
||||
|
new ReadOnlySlice<ushort>(glyphIndices), |
||||
|
new ReadOnlySlice<double>(glyphAdvances), |
||||
|
new ReadOnlySlice<Vector>(glyphOffsets), |
||||
|
text, |
||||
|
new ReadOnlySlice<ushort>(clusters)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,69 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Fonts; |
||||
|
using Avalonia.Platform; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests |
||||
|
{ |
||||
|
public class CustomFontManagerImpl : IFontManagerImpl |
||||
|
{ |
||||
|
private readonly Typeface[] _customTypefaces; |
||||
|
|
||||
|
private readonly Typeface _defaultTypeface = |
||||
|
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); |
||||
|
private readonly Typeface _emojiTypeface = |
||||
|
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); |
||||
|
|
||||
|
public CustomFontManagerImpl() |
||||
|
{ |
||||
|
_customTypefaces = new[] { _emojiTypeface, _defaultTypeface }; |
||||
|
} |
||||
|
|
||||
|
public string GetDefaultFontFamilyName() |
||||
|
{ |
||||
|
return _defaultTypeface.FontFamily.ToString(); |
||||
|
} |
||||
|
|
||||
|
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) |
||||
|
{ |
||||
|
return _customTypefaces.Select(x => x.FontFamily.Name); |
||||
|
} |
||||
|
|
||||
|
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, |
||||
|
CultureInfo culture, out FontKey fontKey) |
||||
|
{ |
||||
|
foreach (var customTypeface in _customTypefaces) |
||||
|
{ |
||||
|
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) |
||||
|
continue; |
||||
|
fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
var fallback = SKFontManager.Default.MatchCharacter(codepoint); |
||||
|
|
||||
|
fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) |
||||
|
{ |
||||
|
switch (typeface.FontFamily.Name) |
||||
|
{ |
||||
|
case "Twitter Color Emoji": |
||||
|
case "Noto Mono": |
||||
|
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); |
||||
|
var skTypeface = typefaceCollection.Get(typeface); |
||||
|
return new GlyphTypefaceImpl(skTypeface); |
||||
|
default: |
||||
|
return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name, |
||||
|
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests |
||||
|
{ |
||||
|
public class SKTypefaceCollectionCacheTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_Load_Typefaces_From_Invalid_Name() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
||||
|
{ |
||||
|
var notoMono = |
||||
|
new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); |
||||
|
|
||||
|
var colorEmoji = |
||||
|
new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); |
||||
|
|
||||
|
var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); |
||||
|
|
||||
|
var typeface = new Typeface("ABC", FontWeight.Bold, FontStyle.Italic); |
||||
|
|
||||
|
Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName); |
||||
|
|
||||
|
var notoColorEmojiCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(colorEmoji); |
||||
|
|
||||
|
Assert.Equal("Twitter Color Emoji", notoColorEmojiCollection.Get(typeface).FamilyName); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,269 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.Utility; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests |
||||
|
{ |
||||
|
public class SimpleTextFormatterTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_Default_Style() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
||||
|
|
||||
|
var textSource = new SimpleTextSource(text, defaultTextRunStyle); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
Assert.Single(textLine.TextRuns); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[0]; |
||||
|
|
||||
|
Assert.Equal(defaultTextRunStyle.TextFormat, textRun.Style.TextFormat); |
||||
|
|
||||
|
Assert.Equal(defaultTextRunStyle.Foreground, textRun.Style.Foreground); |
||||
|
|
||||
|
Assert.Equal(text.Length, textRun.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_Multiple_Buffers() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
||||
|
|
||||
|
var textSource = new MultipleBufferTextSource(defaultTextRunStyle); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new TextParagraphProperties(defaultTextRunStyle)); |
||||
|
|
||||
|
Assert.Equal(5, textLine.TextRuns.Count); |
||||
|
|
||||
|
Assert.Equal(50, textLine.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class MultipleBufferTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly string[] _runTexts; |
||||
|
private readonly TextStyle _defaultStyle; |
||||
|
|
||||
|
public MultipleBufferTextSource(TextStyle defaultStyle) |
||||
|
{ |
||||
|
_defaultStyle = defaultStyle; |
||||
|
|
||||
|
_runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
if (textSourceIndex == 50) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
var index = textSourceIndex / 10; |
||||
|
|
||||
|
var runText = _runTexts[index]; |
||||
|
|
||||
|
return new TextCharacters( |
||||
|
new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_TextRunStyles() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var defaultStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
||||
|
|
||||
|
var textStyleRuns = new[] |
||||
|
{ |
||||
|
new TextStyleRun(new TextPointer(0, 3), defaultStyle ), |
||||
|
new TextStyleRun(new TextPointer(3, 3), new TextStyle(Typeface.Default, 13, Brushes.Black) ), |
||||
|
new TextStyleRun(new TextPointer(6, 3), new TextStyle(Typeface.Default, 14, Brushes.Black) ), |
||||
|
new TextStyleRun(new TextPointer(9, 1), defaultStyle ) |
||||
|
}; |
||||
|
|
||||
|
var textSource = new FormattableTextSource(text, defaultStyle, textStyleRuns); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
Assert.Equal(text.Length, textLine.Text.Length); |
||||
|
|
||||
|
for (var i = 0; i < textStyleRuns.Length; i++) |
||||
|
{ |
||||
|
var textStyleRun = textStyleRuns[i]; |
||||
|
|
||||
|
var textRun = textLine.TextRuns[i]; |
||||
|
|
||||
|
Assert.Equal(textStyleRun.TextPointer.Length, textRun.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class FormattableTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly TextStyle _defaultStyle; |
||||
|
private ReadOnlySlice<TextStyleRun> _textStyleRuns; |
||||
|
|
||||
|
public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice<TextStyleRun> textStyleRuns) |
||||
|
{ |
||||
|
_text = text.AsMemory(); |
||||
|
|
||||
|
_defaultStyle = defaultStyle; |
||||
|
|
||||
|
_textStyleRuns = textStyleRuns; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
if (_textStyleRuns.IsEmpty) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
var styleRun = _textStyleRuns[0]; |
||||
|
|
||||
|
_textStyleRuns = _textStyleRuns.Skip(1); |
||||
|
|
||||
|
return new TextCharacters(_text.AsSlice(styleRun.TextPointer.Start, styleRun.TextPointer.Length), |
||||
|
_defaultStyle); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("0123", 1)] |
||||
|
[InlineData("\r\n", 1)] |
||||
|
[InlineData("👍b", 2)] |
||||
|
[InlineData("a👍b", 3)] |
||||
|
[InlineData("a👍子b", 4)] |
||||
|
public void Should_Produce_Unique_Runs(string text, int numberOfRuns) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
Assert.Equal(numberOfRuns, textLine.TextRuns.Count); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class SimpleTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly TextStyle _defaultTextStyle; |
||||
|
|
||||
|
public SimpleTextSource(string text, TextStyle defaultText) |
||||
|
{ |
||||
|
_text = text.AsMemory(); |
||||
|
_defaultTextStyle = defaultText; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
var runText = _text.Skip(textSourceIndex); |
||||
|
|
||||
|
if (runText.IsEmpty) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
return new TextCharacters(runText, _defaultTextStyle); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Split_Run_On_Direction() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "1234الدولي"; |
||||
|
|
||||
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
Assert.Equal(4, textLine.TextRuns[0].Text.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Get_Distance_From_CharacterHit() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(text.Length)); |
||||
|
|
||||
|
Assert.Equal(textLine.LineMetrics.Size.Width, distance); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Get_CharacterHit_From_Distance() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
||||
|
|
||||
|
var formatter = new SimpleTextFormatter(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
||||
|
|
||||
|
var characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); |
||||
|
|
||||
|
Assert.Equal(textLine.Text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Start() |
||||
|
{ |
||||
|
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface |
||||
|
.With(renderInterface: new PlatformRenderInterface(null), |
||||
|
textShaperImpl: new TextShaperImpl())); |
||||
|
|
||||
|
AvaloniaLocator.CurrentMutable |
||||
|
.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl())); |
||||
|
|
||||
|
return disposable; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,486 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests |
||||
|
{ |
||||
|
public class TextLayoutTests |
||||
|
{ |
||||
|
private static readonly string s_singleLineText = "0123456789"; |
||||
|
private static readonly string s_multiLineText = "012345678\r\r0123456789"; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextStyleSpan_To_Text_In_Between() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(1, 2), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textStyleOverrides : spans); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
Assert.Equal(3, textLine.TextRuns.Count); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[1]; |
||||
|
|
||||
|
Assert.Equal(2, textRun.Text.Length); |
||||
|
|
||||
|
var actual = textRun.Text.Buffer.Span.ToString(); |
||||
|
|
||||
|
Assert.Equal("12", actual); |
||||
|
|
||||
|
Assert.Equal(foreground, textRun.Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
for (var i = 4; i < s_multiLineText.Length; i++) |
||||
|
{ |
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(0, i), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var expected = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textWrapping: TextWrapping.Wrap, |
||||
|
maxWidth : 25); |
||||
|
|
||||
|
var actual = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textWrapping : TextWrapping.Wrap, |
||||
|
maxWidth : 25, |
||||
|
textStyleOverrides : spans); |
||||
|
|
||||
|
Assert.Equal(expected.TextLines.Count, actual.TextLines.Count); |
||||
|
|
||||
|
for (var j = 0; j < actual.TextLines.Count; j++) |
||||
|
{ |
||||
|
Assert.Equal(expected.TextLines[j].Text.Length, actual.TextLines[j].Text.Length); |
||||
|
|
||||
|
Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length), |
||||
|
actual.TextLines[j].TextRuns.Sum(x => x.Text.Length)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextStyleSpan_To_Text_At_Start() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(0, 2), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
s_singleLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textStyleOverrides : spans); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
Assert.Equal(2, textLine.TextRuns.Count); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[0]; |
||||
|
|
||||
|
Assert.Equal(2, textRun.Text.Length); |
||||
|
|
||||
|
var actual = s_singleLineText.Substring(textRun.Text.Start, |
||||
|
textRun.Text.Length); |
||||
|
|
||||
|
Assert.Equal("01", actual); |
||||
|
|
||||
|
Assert.Equal(foreground, textRun.Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextStyleSpan_To_Text_At_End() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(8, 2), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
s_singleLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textStyleOverrides : spans); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
Assert.Equal(2, textLine.TextRuns.Count); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[1]; |
||||
|
|
||||
|
Assert.Equal(2, textRun.Text.Length); |
||||
|
|
||||
|
var actual = textRun.Text.Buffer.Span.ToString(); |
||||
|
|
||||
|
Assert.Equal("89", actual); |
||||
|
|
||||
|
Assert.Equal(foreground, textRun.Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextStyleSpan_To_Single_Character() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(0, 1), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
"0", |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textStyleOverrides : spans); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
Assert.Equal(1, textLine.TextRuns.Count); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[0]; |
||||
|
|
||||
|
Assert.Equal(1, textRun.Text.Length); |
||||
|
|
||||
|
Assert.Equal(foreground, textRun.Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextSpan_To_Unicode_String_In_Between() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "😄😄😄😄"; |
||||
|
|
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(2, 2), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textStyleOverrides: spans); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
Assert.Equal(3, textLine.TextRuns.Count); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[1]; |
||||
|
|
||||
|
Assert.Equal(2, textRun.Text.Length); |
||||
|
|
||||
|
var actual = textRun.Text.Buffer.Span.ToString(); |
||||
|
|
||||
|
Assert.Equal("😄", actual); |
||||
|
|
||||
|
Assert.Equal(foreground, textRun.Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void TextLength_Should_Be_Equal_To_TextLine_Length_Sum() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var layout = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.Text.Length)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var layout = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
Assert.Equal( |
||||
|
s_multiLineText.Length, |
||||
|
layout.TextLines.Select(textLine => |
||||
|
textLine.TextRuns.Sum(textRun => textRun.Text.Length)) |
||||
|
.Sum()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum_After_Wrap_With_Style_Applied() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = |
||||
|
"Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " + |
||||
|
"Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " + |
||||
|
"Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est."; |
||||
|
|
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(0, 24), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
textWrapping : TextWrapping.Wrap, |
||||
|
maxWidth : 180, |
||||
|
textStyleOverrides: spans); |
||||
|
|
||||
|
Assert.Equal( |
||||
|
text.Length, |
||||
|
layout.TextLines.Select(textLine => |
||||
|
textLine.TextRuns.Sum(textRun => textRun.Text.Length)) |
||||
|
.Sum()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Apply_TextStyleSpan_To_MultiLine() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); |
||||
|
|
||||
|
var spans = new[] |
||||
|
{ |
||||
|
new TextStyleRun( |
||||
|
new TextPointer(5, 20), |
||||
|
new TextStyle(Typeface.Default, 12, foreground)) |
||||
|
}; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
s_multiLineText, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable(), |
||||
|
maxWidth : 200, |
||||
|
maxHeight : 125, |
||||
|
textStyleOverrides: spans); |
||||
|
|
||||
|
Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground); |
||||
|
Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Style.Foreground); |
||||
|
Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Style.Foreground); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Hit_Test_SurrogatePair() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "😄😄"; |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0]; |
||||
|
|
||||
|
var glyphRun = shapedRun.GlyphRun; |
||||
|
|
||||
|
var width = glyphRun.Bounds.Width; |
||||
|
|
||||
|
var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); |
||||
|
|
||||
|
Assert.Equal(2, characterHit.FirstCharacterIndex); |
||||
|
|
||||
|
Assert.Equal(2, characterHit.TrailingLength); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("☝🏿", new ushort[] { 0 })] |
||||
|
[InlineData("☝🏿 ab", new ushort[] { 0, 3, 4, 5 })] |
||||
|
[InlineData("ab ☝🏿", new ushort[] { 0, 1, 2, 3 })] |
||||
|
public void Should_Create_Valid_Clusters_For_Text(string text, ushort[] clusters) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
var index = 0; |
||||
|
|
||||
|
foreach (var textRun in textLine.TextRuns) |
||||
|
{ |
||||
|
var shapedRun = (ShapedTextRun)textRun; |
||||
|
|
||||
|
var glyphRun = shapedRun.GlyphRun; |
||||
|
|
||||
|
var glyphClusters = glyphRun.GlyphClusters; |
||||
|
|
||||
|
var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); |
||||
|
|
||||
|
Assert.Equal(expected, glyphRun.GlyphClusters); |
||||
|
|
||||
|
index += glyphClusters.Length; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("abcde\r\n")] |
||||
|
[InlineData("abcde\n\r")] |
||||
|
public void Should_Break_With_BreakChar_Pair(string text) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
Assert.Equal(2, layout.TextLines.Count); |
||||
|
|
||||
|
Assert.Equal(1, layout.TextLines[0].TextRuns.Count); |
||||
|
|
||||
|
Assert.Equal(7, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length); |
||||
|
|
||||
|
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); |
||||
|
|
||||
|
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Have_One_Run_With_Common_Script() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var layout = new TextLayout( |
||||
|
"abcde\r\n", |
||||
|
Typeface.Default, |
||||
|
12.0f, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
Assert.Equal(1, layout.TextLines[0].TextRuns.Count); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Layout_Corrupted_Text() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var text = new string(new[] { '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802' }); |
||||
|
|
||||
|
var layout = new TextLayout( |
||||
|
text, |
||||
|
Typeface.Default, |
||||
|
12, |
||||
|
Brushes.Black.ToImmutable()); |
||||
|
|
||||
|
var textLine = layout.TextLines[0]; |
||||
|
|
||||
|
var textRun = (ShapedTextRun)textLine.TextRuns[0]; |
||||
|
|
||||
|
Assert.Equal(7, textRun.Text.Length); |
||||
|
|
||||
|
var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); |
||||
|
|
||||
|
foreach (var glyph in textRun.GlyphRun.GlyphIndices) |
||||
|
{ |
||||
|
Assert.Equal(replacementGlyph, glyph); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Start() |
||||
|
{ |
||||
|
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface |
||||
|
.With(renderInterface: new PlatformRenderInterface(null), |
||||
|
textShaperImpl: new TextShaperImpl(), |
||||
|
fontManagerImpl : new CustomFontManagerImpl())); |
||||
|
|
||||
|
return disposable; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public class MockTextShaperImpl : ITextShaperImpl |
||||
|
{ |
||||
|
public GlyphRun ShapeText(ReadOnlySlice<char> text, TextFormat textFormat) |
||||
|
{ |
||||
|
var glyphTypeface = textFormat.Typeface.GlyphTypeface; |
||||
|
var glyphIndices = new ushort[text.Length]; |
||||
|
var height = textFormat.FontMetrics.LineHeight; |
||||
|
var width = 0.0; |
||||
|
|
||||
|
for (var i = 0; i < text.Length;) |
||||
|
{ |
||||
|
var index = i; |
||||
|
|
||||
|
var codepoint = Codepoint.ReadAt(text, i, out var count); |
||||
|
|
||||
|
i += count; |
||||
|
|
||||
|
var glyph = glyphTypeface.GetGlyph(codepoint); |
||||
|
|
||||
|
glyphIndices[index] = glyph; |
||||
|
|
||||
|
width += glyphTypeface.GetGlyphAdvance(glyph); |
||||
|
} |
||||
|
|
||||
|
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text, |
||||
|
bounds: new Rect(0, 0, width, height)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Visuals.UnitTests.Media |
||||
|
{ |
||||
|
public class TextDecorationTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_Parse_TextDecorations() |
||||
|
{ |
||||
|
var baseline = TextDecorationCollection.Parse("baseline"); |
||||
|
|
||||
|
Assert.Equal(TextDecorationLocation.Baseline, baseline[0].Location); |
||||
|
|
||||
|
var underline = TextDecorationCollection.Parse("underline"); |
||||
|
|
||||
|
Assert.Equal(TextDecorationLocation.Underline, underline[0].Location); |
||||
|
|
||||
|
var overline = TextDecorationCollection.Parse("overline"); |
||||
|
|
||||
|
Assert.Equal(TextDecorationLocation.Overline, overline[0].Location); |
||||
|
|
||||
|
var strikethrough = TextDecorationCollection.Parse("strikethrough"); |
||||
|
|
||||
|
Assert.Equal(TextDecorationLocation.Strikethrough, strikethrough[0].Location); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ |
||||
|
OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |
||||
|
CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % |
||||
|
GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % |
||||
|
NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % |
||||
|
PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % |
||||
|
B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ |
||||
|
CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % |
||||
|
H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % |
||||
|
H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % |
||||
|
JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % |
||||
|
JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % |
||||
|
JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % |
||||
|
RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % |
||||
|
EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % |
||||
|
EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % |
||||
|
ZWJ _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ % _ % % _ _ ^ # ^ _ _ _ _ _ _ % % % |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue