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