Text API sample app (#19455)
* xml documentatation for paragraph properties * TextTestApp * text test files renamed parameter * glyph ink bounds * shaped buffer make selected row visible * text test app force light theme * text test app distinguish start and end distance marks --------- Co-authored-by: Jan Kučera <miloush@users.noreply.github.com> Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com> Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"samples\\ControlCatalog\\ControlCatalog.csproj",
|
||||
"samples\\GpuInterop\\GpuInterop.csproj",
|
||||
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
|
||||
"samples\\TextTestApp\\TextTestApp.csproj",
|
||||
"samples\\MiniMvvm\\MiniMvvm.csproj",
|
||||
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
|
||||
"samples\\RenderDemo\\RenderDemo.csproj",
|
||||
|
||||
@@ -191,6 +191,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextTestApp", "samples\TextTestApp\TextTestApp.csproj", "{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Browser", "Browser", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
|
||||
@@ -538,6 +540,10 @@ Global
|
||||
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -761,6 +767,7 @@ Global
|
||||
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
|
||||
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<Application xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TextTestApp.App" RequestedThemeVariant="Light">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
desktop.MainWindow = new MainWindow();
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
public class GridRow : Grid
|
||||
{
|
||||
protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.ChildrenChanged(sender, e);
|
||||
|
||||
while (Children.Count > ColumnDefinitions.Count)
|
||||
ColumnDefinitions.Add(new ColumnDefinition { SharedSizeGroup = "c" + ColumnDefinitions.Count });
|
||||
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
SetColumn(Children[i], i);
|
||||
if (Children[i] is Layoutable l)
|
||||
l.VerticalAlignment = VerticalAlignment.Center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
public class InteractiveLineControl : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Text" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<string?> TextProperty =
|
||||
TextBlock.TextProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Background"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IBrush?> BackgroundProperty =
|
||||
Border.BackgroundProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ExtentStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(ExtentStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> BaselineStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BaselineStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> TextBoundsStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(TextBoundsStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> RunBoundsStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(RunBoundsStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> NextHitStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(NextHitStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> BackspaceHitStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BackspaceHitStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> PreviousHitStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(PreviousHitStroke));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> DistanceStrokeProperty =
|
||||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(DistanceStroke));
|
||||
|
||||
public IBrush? ExtentStroke
|
||||
{
|
||||
get => GetValue(ExtentStrokeProperty);
|
||||
set => SetValue(ExtentStrokeProperty, value);
|
||||
}
|
||||
public IBrush? BaselineStroke
|
||||
{
|
||||
get => GetValue(BaselineStrokeProperty);
|
||||
set => SetValue(BaselineStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? TextBoundsStroke
|
||||
{
|
||||
get => GetValue(TextBoundsStrokeProperty);
|
||||
set => SetValue(TextBoundsStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? RunBoundsStroke
|
||||
{
|
||||
get => GetValue(RunBoundsStrokeProperty);
|
||||
set => SetValue(RunBoundsStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? NextHitStroke
|
||||
{
|
||||
get => GetValue(NextHitStrokeProperty);
|
||||
set => SetValue(NextHitStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? BackspaceHitStroke
|
||||
{
|
||||
get => GetValue(BackspaceHitStrokeProperty);
|
||||
set => SetValue(BackspaceHitStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? PreviousHitStroke
|
||||
{
|
||||
get => GetValue(PreviousHitStrokeProperty);
|
||||
set => SetValue(PreviousHitStrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? DistanceStroke
|
||||
{
|
||||
get => GetValue(DistanceStrokeProperty);
|
||||
set => SetValue(DistanceStrokeProperty, value);
|
||||
}
|
||||
|
||||
private IPen? _extentPen;
|
||||
protected IPen ExtentPen => _extentPen ??= new Pen(ExtentStroke, dashStyle: DashStyle.Dash);
|
||||
|
||||
private IPen? _baselinePen;
|
||||
protected IPen BaselinePen => _baselinePen ??= new Pen(BaselineStroke);
|
||||
|
||||
private IPen? _textBoundsPen;
|
||||
protected IPen TextBoundsPen => _textBoundsPen ??= new Pen(TextBoundsStroke);
|
||||
|
||||
private IPen? _runBoundsPen;
|
||||
protected IPen RunBoundsPen => _runBoundsPen ??= new Pen(RunBoundsStroke, dashStyle: DashStyle.Dash);
|
||||
|
||||
private IPen? _nextHitPen;
|
||||
protected IPen NextHitPen => _nextHitPen ??= new Pen(NextHitStroke);
|
||||
|
||||
private IPen? _previousHitPen;
|
||||
protected IPen PreviousHitPen => _previousHitPen ??= new Pen(PreviousHitStroke);
|
||||
|
||||
private IPen? _backspaceHitPen;
|
||||
protected IPen BackspaceHitPen => _backspaceHitPen ??= new Pen(BackspaceHitStroke);
|
||||
|
||||
private IPen? _distancePen;
|
||||
protected IPen DistancePen => _distancePen ??= new Pen(DistanceStroke);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text to draw.
|
||||
/// </summary>
|
||||
public string? Text
|
||||
{
|
||||
get => GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brush used to paint the control's background.
|
||||
/// </summary>
|
||||
public IBrush? Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
// TextRunProperties
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontFamily"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontFeaturesProperty"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
|
||||
TextElement.FontFeaturesProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontSize"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontStyle"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FontStyle> FontStyleProperty =
|
||||
TextElement.FontStyleProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontWeight"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FontWeight> FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FontWeight"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FontStretch> FontStretchProperty =
|
||||
TextElement.FontStretchProperty.AddOwner<InteractiveLineControl>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font family used to draw the control's text.
|
||||
/// </summary>
|
||||
public FontFamily FontFamily
|
||||
{
|
||||
get => GetValue(FontFamilyProperty);
|
||||
set => SetValue(FontFamilyProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font features turned on/off.
|
||||
/// </summary>
|
||||
public FontFeatureCollection? FontFeatures
|
||||
{
|
||||
get => GetValue(FontFeaturesProperty);
|
||||
set => SetValue(FontFeaturesProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the control's text in points.
|
||||
/// </summary>
|
||||
public double FontSize
|
||||
{
|
||||
get => GetValue(FontSizeProperty);
|
||||
set => SetValue(FontSizeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font style used to draw the control's text.
|
||||
/// </summary>
|
||||
public FontStyle FontStyle
|
||||
{
|
||||
get => GetValue(FontStyleProperty);
|
||||
set => SetValue(FontStyleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font weight used to draw the control's text.
|
||||
/// </summary>
|
||||
public FontWeight FontWeight
|
||||
{
|
||||
get => GetValue(FontWeightProperty);
|
||||
set => SetValue(FontWeightProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font stretch used to draw the control's text.
|
||||
/// </summary>
|
||||
public FontStretch FontStretch
|
||||
{
|
||||
get => GetValue(FontStretchProperty);
|
||||
set => SetValue(FontStretchProperty, value);
|
||||
}
|
||||
|
||||
private GenericTextRunProperties? _textRunProperties;
|
||||
public GenericTextRunProperties TextRunProperties
|
||||
{
|
||||
get
|
||||
{
|
||||
return _textRunProperties ??= CreateTextRunProperties();
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
_textRunProperties = value;
|
||||
SetCurrentValue(FontFamilyProperty, value.Typeface.FontFamily);
|
||||
SetCurrentValue(FontFeaturesProperty, value.FontFeatures);
|
||||
SetCurrentValue(FontSizeProperty, value.FontRenderingEmSize);
|
||||
SetCurrentValue(FontStyleProperty, value.Typeface.Style);
|
||||
SetCurrentValue(FontWeightProperty, value.Typeface.Weight);
|
||||
SetCurrentValue(FontStretchProperty, value.Typeface.Stretch);
|
||||
}
|
||||
}
|
||||
|
||||
private GenericTextRunProperties CreateTextRunProperties()
|
||||
{
|
||||
Typeface typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
|
||||
return new GenericTextRunProperties(typeface, FontFeatures, FontSize,
|
||||
textDecorations: null,
|
||||
foregroundBrush: Brushes.Black,
|
||||
backgroundBrush: null,
|
||||
baselineAlignment: BaselineAlignment.Baseline,
|
||||
cultureInfo: null);
|
||||
}
|
||||
|
||||
// TextParagraphProperties
|
||||
|
||||
private GenericTextParagraphProperties? _textParagraphProperties;
|
||||
public GenericTextParagraphProperties TextParagraphProperties
|
||||
{
|
||||
get
|
||||
{
|
||||
return _textParagraphProperties ??= CreateTextParagraphProperties();
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
_textParagraphProperties = null;
|
||||
SetCurrentValue(FlowDirectionProperty, value.FlowDirection);
|
||||
}
|
||||
}
|
||||
|
||||
private GenericTextParagraphProperties CreateTextParagraphProperties()
|
||||
{
|
||||
return new GenericTextParagraphProperties(
|
||||
FlowDirection,
|
||||
TextAlignment.Start,
|
||||
firstLineInParagraph: false,
|
||||
alwaysCollapsible: false,
|
||||
TextRunProperties,
|
||||
textWrapping: TextWrapping.NoWrap,
|
||||
lineHeight: 0,
|
||||
indent: 0,
|
||||
letterSpacing: 0);
|
||||
}
|
||||
|
||||
private readonly ITextSource _textSource;
|
||||
private class TextSource : ITextSource
|
||||
{
|
||||
private readonly InteractiveLineControl _owner;
|
||||
|
||||
public TextSource(InteractiveLineControl owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public TextRun? GetTextRun(int textSourceIndex)
|
||||
{
|
||||
string text = _owner.Text ?? string.Empty;
|
||||
|
||||
if (textSourceIndex < 0 || textSourceIndex >= text.Length)
|
||||
return null;
|
||||
|
||||
return new TextCharacters(text, _owner.TextRunProperties);
|
||||
}
|
||||
}
|
||||
|
||||
private TextLine? _textLine;
|
||||
public TextLine? TextLine => _textLine ??= TextFormatter.Current.FormatLine(_textSource, 0, Bounds.Size.Width, TextParagraphProperties);
|
||||
|
||||
private TextLayout? _textLayout;
|
||||
public TextLayout TextLayout => _textLayout ??= new TextLayout(_textSource, TextParagraphProperties);
|
||||
|
||||
private Size? _textLineSize;
|
||||
protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default;
|
||||
|
||||
private Size? _inkSize;
|
||||
protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace + textLine.OverhangTrailing, textLine.Extent) : default;
|
||||
|
||||
public event EventHandler? TextLineChanged;
|
||||
|
||||
public InteractiveLineControl()
|
||||
{
|
||||
_textSource = new TextSource(this);
|
||||
|
||||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
||||
RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
|
||||
}
|
||||
|
||||
private void InvalidateTextRunProperties()
|
||||
{
|
||||
_textRunProperties = null;
|
||||
InvalidateTextParagraphProperties();
|
||||
}
|
||||
|
||||
private void InvalidateTextParagraphProperties()
|
||||
{
|
||||
_textParagraphProperties = null;
|
||||
InvalidateTextLine();
|
||||
}
|
||||
|
||||
private void InvalidateTextLine()
|
||||
{
|
||||
_textLayout = null;
|
||||
_textLine = null;
|
||||
_textLineSize = null;
|
||||
_inkSize = null;
|
||||
InvalidateMeasure();
|
||||
InvalidateVisual();
|
||||
|
||||
TextLineChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
switch (change.Property.Name)
|
||||
{
|
||||
case nameof(FontFamily):
|
||||
case nameof(FontSize):
|
||||
InvalidateTextRunProperties();
|
||||
break;
|
||||
|
||||
case nameof(FontStyle):
|
||||
case nameof(FontWeight):
|
||||
case nameof(FontStretch):
|
||||
InvalidateTextRunProperties();
|
||||
break;
|
||||
|
||||
case nameof(FlowDirection):
|
||||
InvalidateTextParagraphProperties();
|
||||
break;
|
||||
|
||||
case nameof(Text):
|
||||
InvalidateTextLine();
|
||||
break;
|
||||
|
||||
case nameof(BaselineStroke):
|
||||
_baselinePen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
|
||||
case nameof(TextBoundsStroke):
|
||||
_textBoundsPen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
|
||||
case nameof(RunBoundsStroke):
|
||||
_runBoundsPen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
|
||||
case nameof(NextHitStroke):
|
||||
_nextHitPen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
|
||||
case nameof(PreviousHitStroke):
|
||||
_previousHitPen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
|
||||
case nameof(BackspaceHitStroke):
|
||||
_backspaceHitPen = null;
|
||||
InvalidateVisual();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
if (TextLine == null)
|
||||
return default;
|
||||
|
||||
return new Size(Math.Max(TextLineSize.Width, InkSize.Width), Math.Max(TextLineSize.Height, InkSize.Height));
|
||||
}
|
||||
|
||||
private const double VerticalSpacing = 5;
|
||||
private const double HorizontalSpacing = 5;
|
||||
private const double ArrowSize = 5;
|
||||
|
||||
private Dictionary<string, FormattedText> _labelsCache = new();
|
||||
protected FormattedText GetOrCreateLabel(string label, IBrush brush, bool disableCache = false)
|
||||
{
|
||||
if (_labelsCache.TryGetValue(label, out var text))
|
||||
return text;
|
||||
|
||||
text = new FormattedText(label, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Typeface.Default, 8, brush);
|
||||
|
||||
if (!disableCache)
|
||||
_labelsCache[label] = text;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private Rect _inkRenderBounds;
|
||||
private Rect _lineRenderBounds;
|
||||
|
||||
public Rect InkRenderBounds => _inkRenderBounds;
|
||||
public Rect LineRenderBounds => _lineRenderBounds;
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
TextLine? textLine = TextLine;
|
||||
if (textLine == null)
|
||||
return;
|
||||
|
||||
// overhang leading should be negative when extending (e.g. for j) WPF: "When the leading alignment point comes before the leading drawn pixel, the value is negative." - docs wrong but values correct
|
||||
// overhang trailing should be negative when extending (e.g. for f) WPF: "The OverhangTrailing value will be positive when the trailing drawn pixel comes before the trailing alignment point."
|
||||
// overhang after should be negative when inside (e.g. for x) WPF: "The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line."
|
||||
// => we want overhang before to be negative when inside (e.g. for x)
|
||||
|
||||
double overhangBefore = textLine.Extent - textLine.OverhangAfter - textLine.Height;
|
||||
Rect inkBounds = new Rect(new Point(textLine.OverhangLeading, -overhangBefore), InkSize);
|
||||
Rect lineBounds = new Rect(new Point(0, 0), TextLineSize);
|
||||
|
||||
if (inkBounds.Left < 0)
|
||||
lineBounds = lineBounds.Translate(new Vector(-inkBounds.Left, 0));
|
||||
|
||||
if (inkBounds.Top < 0)
|
||||
lineBounds = lineBounds.Translate(new Vector(0, -inkBounds.Top));
|
||||
|
||||
_inkRenderBounds = inkBounds;
|
||||
_lineRenderBounds = lineBounds;
|
||||
|
||||
Rect bounds = new Rect(0, 0, Math.Max(inkBounds.Right, lineBounds.Right), Math.Max(inkBounds.Bottom, lineBounds.Bottom));
|
||||
double labelX = bounds.Right + HorizontalSpacing;
|
||||
|
||||
if (Background is IBrush background)
|
||||
context.FillRectangle(background, lineBounds);
|
||||
|
||||
if (ExtentStroke != null)
|
||||
{
|
||||
context.DrawRectangle(ExtentPen, inkBounds);
|
||||
RenderLabel(context, nameof(textLine.Extent), ExtentStroke, labelX, inkBounds.Top);
|
||||
}
|
||||
|
||||
using (context.PushTransform(Matrix.CreateTranslation(lineBounds.Left, lineBounds.Top)))
|
||||
{
|
||||
labelX -= lineBounds.Left; // labels to ignore horizontal transform
|
||||
|
||||
if (BaselineStroke != null)
|
||||
{
|
||||
RenderFontLine(context, textLine.Baseline, lineBounds.Width, BaselinePen); // no other lines currently available in Avalonia
|
||||
RenderLabel(context, nameof(textLine.Baseline), BaselineStroke, labelX, textLine.Baseline);
|
||||
}
|
||||
|
||||
textLine.Draw(context, lineOrigin: default);
|
||||
|
||||
var runBoundsStroke = RunBoundsStroke;
|
||||
if (TextBoundsStroke != null || runBoundsStroke != null)
|
||||
{
|
||||
IReadOnlyList<TextBounds> textBounds = textLine.GetTextBounds(textLine.FirstTextSourceIndex, textLine.Length);
|
||||
foreach (var textBound in textBounds)
|
||||
{
|
||||
if (runBoundsStroke != null)
|
||||
{
|
||||
var runBounds = textBound.TextRunBounds;
|
||||
foreach (var runBound in runBounds)
|
||||
context.DrawRectangle(RunBoundsPen, runBound.Rectangle);
|
||||
}
|
||||
|
||||
context.DrawRectangle(TextBoundsPen, textBound.Rectangle);
|
||||
}
|
||||
}
|
||||
|
||||
double y = inkBounds.Bottom - lineBounds.Top + VerticalSpacing * 2;
|
||||
|
||||
if (NextHitStroke != null)
|
||||
{
|
||||
RenderHits(context, NextHitPen, textLine, textLine.GetNextCaretCharacterHit, new CharacterHit(0), ref y);
|
||||
RenderLabel(context, nameof(textLine.GetNextCaretCharacterHit), NextHitStroke, labelX, y);
|
||||
y += VerticalSpacing * 2;
|
||||
}
|
||||
|
||||
if (PreviousHitStroke != null)
|
||||
{
|
||||
RenderLabel(context, nameof(textLine.GetPreviousCaretCharacterHit), PreviousHitStroke, labelX, y);
|
||||
RenderHits(context, PreviousHitPen, textLine, textLine.GetPreviousCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
|
||||
y += VerticalSpacing * 2;
|
||||
}
|
||||
|
||||
if (BackspaceHitStroke != null)
|
||||
{
|
||||
RenderLabel(context, nameof(textLine.GetBackspaceCaretCharacterHit), BackspaceHitStroke, labelX, y);
|
||||
RenderHits(context, BackspaceHitPen, textLine, textLine.GetBackspaceCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
|
||||
y += VerticalSpacing * 2;
|
||||
}
|
||||
|
||||
if (DistanceStroke != null)
|
||||
{
|
||||
y += VerticalSpacing;
|
||||
|
||||
var label = RenderLabel(context, nameof(textLine.GetDistanceFromCharacterHit), DistanceStroke, 0, y);
|
||||
y += label.Height;
|
||||
|
||||
for (int i = 0; i < textLine.Length; i++)
|
||||
{
|
||||
var hit = new CharacterHit(i);
|
||||
CharacterHit prevHit = default, nextHit = default;
|
||||
|
||||
double leftLabelX = -HorizontalSpacing;
|
||||
|
||||
// we want z-order to be previous, next, distance
|
||||
// but labels need to be ordered next, distance, previous
|
||||
if (NextHitStroke != null)
|
||||
{
|
||||
nextHit = textLine.GetNextCaretCharacterHit(hit);
|
||||
var nextLabel = RenderLabel(context, $" > {nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}", NextHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
|
||||
leftLabelX -= nextLabel.WidthIncludingTrailingWhitespace;
|
||||
}
|
||||
|
||||
if (PreviousHitStroke != null)
|
||||
{
|
||||
prevHit = textLine.GetPreviousCaretCharacterHit(hit);
|
||||
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex, 0));
|
||||
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex + prevHit.TrailingLength, 0));
|
||||
RenderHorizontalPoint(context, x1, x2, y, PreviousHitPen, ArrowSize);
|
||||
}
|
||||
|
||||
if (NextHitStroke != null)
|
||||
{
|
||||
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex, 0));
|
||||
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex + nextHit.TrailingLength, 0));
|
||||
RenderHorizontalPoint(context, x1, x2, y, NextHitPen, ArrowSize);
|
||||
}
|
||||
|
||||
label = RenderLabel(context, $"[{i}]", DistanceStroke, leftLabelX, y, TextAlignment.Right);
|
||||
leftLabelX -= label.WidthIncludingTrailingWhitespace;
|
||||
|
||||
if (PreviousHitStroke != null)
|
||||
RenderLabel(context, $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength} < ", PreviousHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
|
||||
|
||||
double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(i));
|
||||
RenderHorizontalBar(context, 0, distance, y, DistancePen, ArrowSize);
|
||||
//RenderLabel(context, distance.ToString("F2"), DistanceStroke, distance + HorizontalSpacing, y, disableCache: true);
|
||||
|
||||
y += label.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull("brush")]
|
||||
private FormattedText? RenderLabel(DrawingContext context, string label, IBrush? brush, double x, double y, TextAlignment alignment = TextAlignment.Left, bool disableCache = false)
|
||||
{
|
||||
if (brush == null)
|
||||
return null;
|
||||
|
||||
var text = GetOrCreateLabel(label, brush, disableCache);
|
||||
|
||||
if (alignment == TextAlignment.Right)
|
||||
context.DrawText(text, new Point(x - text.WidthIncludingTrailingWhitespace, y - text.Height / 2));
|
||||
else
|
||||
context.DrawText(text, new Point(x, y - text.Height / 2));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private void RenderHits(DrawingContext context, IPen hitPen, TextLine textLine, Func<CharacterHit, CharacterHit> nextHit, CharacterHit startingHit, ref double y)
|
||||
{
|
||||
CharacterHit lastHit = startingHit;
|
||||
double lastX = textLine.GetDistanceFromCharacterHit(lastHit);
|
||||
double lastDirection = 0;
|
||||
y -= VerticalSpacing; // we always start with adding one below
|
||||
|
||||
while (true)
|
||||
{
|
||||
CharacterHit hit = nextHit(lastHit);
|
||||
if (hit == lastHit)
|
||||
break;
|
||||
|
||||
double x = textLine.GetDistanceFromCharacterHit(hit);
|
||||
double direction = Math.Sign(x - lastX);
|
||||
|
||||
if (direction == 0 || lastDirection != direction)
|
||||
y += VerticalSpacing;
|
||||
|
||||
if (direction == 0)
|
||||
RenderPoint(context, x, y, hitPen, ArrowSize);
|
||||
else
|
||||
RenderHorizontalArrow(context, lastX, x, y, hitPen, ArrowSize);
|
||||
|
||||
lastX = x;
|
||||
lastHit = hit;
|
||||
lastDirection = direction;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderPoint(DrawingContext context, double x, double y, IPen pen, double arrowHeight)
|
||||
{
|
||||
context.DrawEllipse(pen.Brush, pen, new Point(x, y), ArrowSize / 2, ArrowSize / 2);
|
||||
}
|
||||
|
||||
private void RenderHorizontalPoint(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
|
||||
{
|
||||
PathGeometry startCap = new PathGeometry();
|
||||
PathFigure startFigure = new PathFigure();
|
||||
startFigure.StartPoint = new Point(xStart, y - size / 2);
|
||||
startFigure.IsClosed = true;
|
||||
startFigure.IsFilled = true;
|
||||
startFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xStart, y + size / 2), SweepDirection = SweepDirection.CounterClockwise });
|
||||
startCap.Figures!.Add(startFigure);
|
||||
|
||||
context.DrawGeometry(pen.Brush, pen, startCap);
|
||||
|
||||
PathGeometry endCap = new PathGeometry();
|
||||
PathFigure endFigure = new PathFigure();
|
||||
endFigure.StartPoint = new Point(xEnd, y - size / 2);
|
||||
endFigure.IsClosed = true;
|
||||
endFigure.IsFilled = false;
|
||||
endFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xEnd, y + size / 2), SweepDirection = SweepDirection.Clockwise });
|
||||
endCap.Figures!.Add(endFigure);
|
||||
|
||||
context.DrawGeometry(pen.Brush, pen, endCap);
|
||||
}
|
||||
|
||||
private void RenderHorizontalArrow(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
|
||||
{
|
||||
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
|
||||
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
|
||||
|
||||
if (xEnd >= xStart)
|
||||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
|
||||
[
|
||||
new Point(xEnd - size, y - size / 2),
|
||||
new Point(xEnd - size, y + size/2),
|
||||
new Point(xEnd, y)
|
||||
], isFilled: true));
|
||||
else
|
||||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
|
||||
[
|
||||
new Point(xEnd + size, y - size / 2),
|
||||
new Point(xEnd + size, y + size/2),
|
||||
new Point(xEnd, y)
|
||||
], isFilled: true));
|
||||
}
|
||||
private void RenderHorizontalBar(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
|
||||
{
|
||||
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
|
||||
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
|
||||
context.DrawLine(pen, new Point(xEnd, y - size / 2), new Point(xEnd, y + size / 2)); // end cap
|
||||
}
|
||||
|
||||
private void RenderFontLine(DrawingContext context, double y, double width, IPen pen)
|
||||
{
|
||||
context.DrawLine(pen, new Point(0, y), new Point(width, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:TextTestApp"
|
||||
x:Class="TextTestApp.MainWindow"
|
||||
Title="Text Test App" Width="700" Height="700">
|
||||
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Bottom" Background="WhiteSmoke" BorderThickness="0,1,0,0" BorderBrush="Silver" Padding="2">
|
||||
<DockPanel>
|
||||
<ToggleSwitch Name="_hitRangeToggle" DockPanel.Dock="Right" OnContent="HitTestTextRange" OffContent="HitTestTextPosition" IsCheckedChanged="OnHitTestMethodChanged" />
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="HitTestPoint:" Margin="5,0" />
|
||||
<TextBlock Name="_coordinates" MinWidth="120" />
|
||||
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
|
||||
<TextBlock Text="TextPosition:" Margin="5,0" />
|
||||
<TextBlock Name="_hit" MinWidth="60" />
|
||||
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<DockPanel DockPanel.Dock="Top" Margin="5">
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
|
||||
<Label Content="_Font:" Target="{Binding ElementName=_font}" VerticalAlignment="Center" Margin="5,0,0,0" />
|
||||
<ComboBox Name="_font" ItemsSource="{Binding SystemFonts, Source={x:Static FontManager.Current}}" />
|
||||
<Label Content="_Size:" Target="{Binding ElementName=_size}" VerticalAlignment="Center" Margin="5,0,0,0" />
|
||||
<TextBox Name="_size" VerticalAlignment="Center" Text="64" />
|
||||
<Button VerticalAlignment="Center" Click="OnNewWindowClick" ToolTip.Tip="New window" Margin="5,0,0,0">+</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Label Content="_Text:" Target="{Binding ElementName=_text}" VerticalAlignment="Center"/>
|
||||
<TextBox Name="_text" Text="Hello!" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<Grid RowDefinitions="*,5,*">
|
||||
<local:InteractiveLineControl Name="_rendering" DockPanel.Dock="Top" Margin="16" HorizontalAlignment="Center"
|
||||
|
||||
Text="{Binding Text, ElementName=_text}"
|
||||
FontFamily="{Binding SelectedValue, ElementName=_font}"
|
||||
FontSize="{Binding Text, ElementName=_size}"
|
||||
Background="BlanchedAlmond"
|
||||
ExtentStroke="Black"
|
||||
BaselineStroke="Blue"
|
||||
TextBoundsStroke="Goldenrod"
|
||||
RunBoundsStroke="Gold"
|
||||
NextHitStroke="Green"
|
||||
PreviousHitStroke="Blue"
|
||||
BackspaceHitStroke="Red"
|
||||
DistanceStroke="Black"
|
||||
|
||||
PointerMoved="OnPointerMoved"
|
||||
/>
|
||||
|
||||
<GridSplitter Grid.Row="1" />
|
||||
|
||||
<TabControl Grid.Row="2" DockPanel.Dock="Bottom" Background="White" BorderBrush="Whitesmoke" BorderThickness="0,1,0,0">
|
||||
<TabItem Header="Shaped Buffer">
|
||||
<ListBox Name="_buffer" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionMode="Multiple" SelectionChanged="OnBufferSelectionChanged" Background="Transparent">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Background" Value="White" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
|
||||
<local:GridRow ColumnSpacing="10">
|
||||
<TextBlock Text="" />
|
||||
<TextBlock Text="Index" />
|
||||
<TextBlock Text="Characters" />
|
||||
<TextBlock Text="Codepoints" />
|
||||
<TextBlock Text="Glyph" />
|
||||
<TextBlock Text="Glyph ID" />
|
||||
<TextBlock Text="Advance" />
|
||||
<TextBlock Text="Offset" />
|
||||
<TextBlock Text="Ink Bounds" />
|
||||
</local:GridRow>
|
||||
</Border>
|
||||
</ListBox>
|
||||
</TabItem>
|
||||
<TabItem Header="Character Hits">
|
||||
<ListBox Name="_hits" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionChanged="OnHitsSelectionChanged" Background="Transparent">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Background" Value="White" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
|
||||
<local:GridRow ColumnSpacing="10">
|
||||
<TextBlock Text="" />
|
||||
<TextBlock Text="Backspace Hit" />
|
||||
<TextBlock Text="Previous Hit" />
|
||||
<TextBlock Text="Index" />
|
||||
<TextBlock Text="Next Hit" />
|
||||
<TextBlock Text="Codepoint" />
|
||||
<TextBlock Text="Character" />
|
||||
<TextBlock Text="Distance" />
|
||||
</local:GridRow>
|
||||
</Border>
|
||||
</ListBox>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,340 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private SelectionAdorner? _selectionAdorner;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_selectionAdorner = new();
|
||||
_selectionAdorner.Stroke = Brushes.Red;
|
||||
_selectionAdorner.Fill = new SolidColorBrush(Colors.LightSkyBlue, 0.25);
|
||||
_selectionAdorner.IsHitTestVisible = false;
|
||||
AdornerLayer.SetIsClipEnabled(_selectionAdorner, false);
|
||||
AdornerLayer.SetAdorner(_rendering, _selectionAdorner);
|
||||
|
||||
_rendering.TextLineChanged += OnShapeBufferChanged;
|
||||
OnShapeBufferChanged();
|
||||
}
|
||||
|
||||
private void OnNewWindowClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
MainWindow win = new MainWindow();
|
||||
win.Show();
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.F5)
|
||||
{
|
||||
_rendering.InvalidateVisual();
|
||||
OnShapeBufferChanged();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
if (_hits.IsKeyboardFocusWithin && _hits.SelectedIndex != -1)
|
||||
{
|
||||
_hits.SelectedIndex = -1;
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (_buffer.IsKeyboardFocusWithin && _buffer.SelectedIndex != -1)
|
||||
{
|
||||
_buffer.SelectedIndex = -1;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private void OnShapeBufferChanged(object? sender, EventArgs e) => OnShapeBufferChanged();
|
||||
private void OnShapeBufferChanged()
|
||||
{
|
||||
if (_selectionAdorner == null)
|
||||
return;
|
||||
|
||||
ListBuffers();
|
||||
ListHits();
|
||||
|
||||
Rect bounds = _rendering.LineRenderBounds;
|
||||
_selectionAdorner!.Transform = Matrix.CreateTranslation(bounds.X, bounds.Y);
|
||||
}
|
||||
|
||||
private void ListBuffers()
|
||||
{
|
||||
for (int i = _buffer.ItemCount - 1; i >= 1; i--)
|
||||
_buffer.Items.RemoveAt(i);
|
||||
|
||||
TextLine? textLine = _rendering.TextLine;
|
||||
if (textLine == null)
|
||||
return;
|
||||
|
||||
double currentX = _rendering.LineRenderBounds.Left;
|
||||
foreach (TextRun run in textLine.TextRuns)
|
||||
{
|
||||
if (run is ShapedTextRun shapedRun)
|
||||
{
|
||||
_buffer.Items.Add(new TextBlock
|
||||
{
|
||||
Text = $"{run.GetType().Name}: Bidi = {shapedRun.BidiLevel}, Font = {shapedRun.ShapedBuffer.GlyphTypeface.FamilyName}",
|
||||
FontWeight = FontWeight.Bold,
|
||||
Padding = new Thickness(10, 0),
|
||||
Tag = run,
|
||||
});
|
||||
|
||||
ListBuffer(textLine, shapedRun, ref currentX);
|
||||
}
|
||||
else
|
||||
_buffer.Items.Add(new TextBlock
|
||||
{
|
||||
Text = run.GetType().Name,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Padding = new Thickness(10, 0),
|
||||
Tag = run
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ListHits()
|
||||
{
|
||||
for (int i = _hits.ItemCount - 1; i >= 1; i--)
|
||||
_hits.Items.RemoveAt(i);
|
||||
|
||||
TextLine? textLine = _rendering.TextLine;
|
||||
if (textLine == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < textLine.Length; i++)
|
||||
{
|
||||
string? clusterText = _rendering.Text!.Substring(i, 1);
|
||||
string? clusterHex = ToHex(clusterText);
|
||||
|
||||
var hit = new CharacterHit(i);
|
||||
var prevHit = textLine.GetPreviousCaretCharacterHit(hit);
|
||||
var nextHit = textLine.GetNextCaretCharacterHit(hit);
|
||||
var bkspHit = textLine.GetBackspaceCaretCharacterHit(hit);
|
||||
|
||||
GridRow row = new GridRow { ColumnSpacing = 10 };
|
||||
row.Children.Add(new Control());
|
||||
row.Children.Add(new TextBlock { Text = $"{bkspHit.FirstCharacterIndex}+{bkspHit.TrailingLength}" });
|
||||
row.Children.Add(new TextBlock { Text = $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength}" });
|
||||
row.Children.Add(new TextBlock { Text = i.ToString(), FontWeight = FontWeight.Bold });
|
||||
row.Children.Add(new TextBlock { Text = $"{nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}" });
|
||||
row.Children.Add(new TextBlock { Text = clusterHex });
|
||||
row.Children.Add(new TextBlock { Text = clusterText });
|
||||
row.Children.Add(new TextBlock { Text = textLine.GetDistanceFromCharacterHit(hit).ToString() });
|
||||
row.Tag = i;
|
||||
|
||||
_hits.Items.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly IBrush TransparentAliceBlue = new SolidColorBrush(0x0F0188FF);
|
||||
private static readonly IBrush TransparentAntiqueWhite = new SolidColorBrush(0x28DF8000);
|
||||
private void ListBuffer(TextLine textLine, ShapedTextRun shapedRun, ref double currentX)
|
||||
{
|
||||
ShapedBuffer buffer = shapedRun.ShapedBuffer;
|
||||
|
||||
int lastClusterStart = -1;
|
||||
bool oddCluster = false;
|
||||
|
||||
IReadOnlyList<GlyphInfo> glyphInfos = buffer;
|
||||
|
||||
currentX += shapedRun.GlyphRun.BaselineOrigin.X;
|
||||
for (var i = 0; i < glyphInfos.Count; i++)
|
||||
{
|
||||
GlyphInfo info = glyphInfos[i];
|
||||
int clusterStart = info.GlyphCluster;
|
||||
int clusterLength = FindClusterLenghtAt(i);
|
||||
string? clusterText = _rendering.Text!.Substring(clusterStart, clusterLength);
|
||||
string? clusterHex = ToHex(clusterText);
|
||||
|
||||
Border border = new Border();
|
||||
if (clusterStart == lastClusterStart)
|
||||
{
|
||||
clusterText = clusterHex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
oddCluster = !oddCluster;
|
||||
lastClusterStart = clusterStart;
|
||||
}
|
||||
border.Background = oddCluster ? TransparentAliceBlue : TransparentAntiqueWhite;
|
||||
|
||||
|
||||
GridRow row = new GridRow { ColumnSpacing = 10 };
|
||||
row.Children.Add(new Control());
|
||||
row.Children.Add(new TextBlock { Text = clusterStart.ToString() });
|
||||
row.Children.Add(new TextBlock { Text = clusterText });
|
||||
row.Children.Add(new TextBlock { Text = clusterHex, TextWrapping = TextWrapping.Wrap });
|
||||
row.Children.Add(new Image { Source = CreateGlyphDrawing(shapedRun.GlyphRun.GlyphTypeface, FontSize, info), Margin = new Thickness(2) });
|
||||
row.Children.Add(new TextBlock { Text = info.GlyphIndex.ToString() });
|
||||
row.Children.Add(new TextBlock { Text = info.GlyphAdvance.ToString() });
|
||||
row.Children.Add(new TextBlock { Text = info.GlyphOffset.ToString() });
|
||||
|
||||
Geometry glyph = GetGlyphOutline(shapedRun.GlyphRun.GlyphTypeface, shapedRun.GlyphRun.FontRenderingEmSize, info);
|
||||
Rect glyphBounds = glyph.Bounds;
|
||||
Rect offsetBounds = glyphBounds.Translate(new Vector(currentX + info.GlyphOffset.X, info.GlyphOffset.Y));
|
||||
|
||||
TextBlock boundsBlock = new TextBlock { Text = offsetBounds.ToString() };
|
||||
ToolTip.SetTip(boundsBlock, "Origin bounds: " + glyphBounds);
|
||||
row.Children.Add(boundsBlock);
|
||||
|
||||
border.Child = row;
|
||||
border.Tag = offsetBounds;
|
||||
_buffer.Items.Add(border);
|
||||
|
||||
currentX += glyphInfos[i].GlyphAdvance;
|
||||
}
|
||||
|
||||
int FindClusterLenghtAt(int index)
|
||||
{
|
||||
int cluster = glyphInfos[index].GlyphCluster;
|
||||
if (shapedRun.BidiLevel % 2 == 0)
|
||||
{
|
||||
while (++index < glyphInfos.Count)
|
||||
if (glyphInfos[index].GlyphCluster != cluster)
|
||||
return glyphInfos[index].GlyphCluster - cluster;
|
||||
|
||||
return shapedRun.Length + glyphInfos[0].GlyphCluster - cluster;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (--index >= 0)
|
||||
if (glyphInfos[index].GlyphCluster != cluster)
|
||||
return glyphInfos[index].GlyphCluster - cluster;
|
||||
|
||||
return shapedRun.Length + glyphInfos[glyphInfos.Count - 1].GlyphCluster - cluster;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info)
|
||||
{
|
||||
return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } };
|
||||
}
|
||||
|
||||
private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info)
|
||||
{
|
||||
// substitute for GlyphTypeface.GetGlyphOutline
|
||||
return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry();
|
||||
}
|
||||
|
||||
private void OnPointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
InteractiveLineControl lineControl = (InteractiveLineControl)sender;
|
||||
TextLayout textLayout = lineControl.TextLayout;
|
||||
Rect lineBounds = lineControl.LineRenderBounds;
|
||||
|
||||
PointerPoint pointerPoint = e.GetCurrentPoint(lineControl);
|
||||
Point point = new Point(pointerPoint.Position.X - lineBounds.Left, pointerPoint.Position.Y - lineBounds.Top);
|
||||
_coordinates.Text = $"{pointerPoint.Position.X:F4}, {pointerPoint.Position.Y:F4}";
|
||||
|
||||
TextHitTestResult textHit = textLayout.HitTestPoint(point);
|
||||
_hit.Text = $"{textHit.TextPosition} ({textHit.CharacterHit.FirstCharacterIndex}+{textHit.CharacterHit.TrailingLength})";
|
||||
if (textHit.IsTrailing)
|
||||
_hit.Text += " T";
|
||||
|
||||
if (textHit.IsInside)
|
||||
{
|
||||
_hits.SelectedIndex = textHit.TextPosition + 1; // header
|
||||
}
|
||||
else
|
||||
_hits.SelectedIndex = -1;
|
||||
}
|
||||
|
||||
private void OnHitTestMethodChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_hits.SelectionMode = _hitRangeToggle.IsChecked == true ? SelectionMode.Multiple : SelectionMode.Single;
|
||||
}
|
||||
|
||||
private void OnHitsSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_selectionAdorner == null)
|
||||
return;
|
||||
|
||||
List<Rect> rectangles = new List<Rect>();
|
||||
TextLayout textLayout = _rendering.TextLayout;
|
||||
|
||||
if (_hitRangeToggle.IsChecked == true)
|
||||
{
|
||||
// collect continuous selected indices
|
||||
List<(int start, int length)> selections = new(1);
|
||||
|
||||
int[] indices = _hits.Selection.SelectedIndexes.ToArray();
|
||||
Array.Sort(indices);
|
||||
|
||||
int currentIndex = -1;
|
||||
int currentLength = 0;
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
if (_hits.Items[indices[i]] is Control { Tag: int index })
|
||||
{
|
||||
if (index == currentIndex + currentLength)
|
||||
{
|
||||
currentLength++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentLength > 0)
|
||||
selections.Add((currentIndex, currentLength));
|
||||
|
||||
currentIndex = index;
|
||||
currentLength = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLength > 0)
|
||||
selections.Add((currentIndex, currentLength));
|
||||
|
||||
foreach (var selection in selections)
|
||||
{
|
||||
var selectionRectangles = textLayout.HitTestTextRange(selection.start, selection.length);
|
||||
rectangles.AddRange(selectionRectangles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_hits.SelectedItem is Control { Tag: int index })
|
||||
{
|
||||
Rect rect = textLayout.HitTestTextPosition(index);
|
||||
rectangles.Add(rect);
|
||||
}
|
||||
}
|
||||
|
||||
_selectionAdorner.Rectangles = rectangles;
|
||||
}
|
||||
|
||||
private void OnBufferSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
List<Rect> rectangles = new List<Rect>(_buffer.Selection.Count);
|
||||
|
||||
foreach (var row in _buffer.SelectedItems)
|
||||
if (row is Control { Tag: Rect rect })
|
||||
rectangles.Add(rect);
|
||||
|
||||
_selectionAdorner.Rectangles = rectangles;
|
||||
}
|
||||
|
||||
private static string ToHex(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return s;
|
||||
|
||||
return string.Join(" ", s.Select(c => ((int)c).ToString("X4")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
{
|
||||
return AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace TextTestApp
|
||||
{
|
||||
public class SelectionAdorner : Control
|
||||
{
|
||||
public static readonly StyledProperty<IBrush?> FillProperty =
|
||||
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Fill));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> StrokeProperty =
|
||||
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Stroke));
|
||||
|
||||
public static readonly StyledProperty<Matrix> TransformProperty =
|
||||
AvaloniaProperty.Register<SelectionAdorner, Matrix>(nameof(Transform), Matrix.Identity);
|
||||
|
||||
public Matrix Transform
|
||||
{
|
||||
get => this.GetValue(TransformProperty);
|
||||
set => SetValue(TransformProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? Stroke
|
||||
{
|
||||
get => GetValue(StrokeProperty);
|
||||
set => SetValue(StrokeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? Fill
|
||||
{
|
||||
get => GetValue(FillProperty);
|
||||
set => SetValue(FillProperty, value);
|
||||
}
|
||||
|
||||
private IList<Rect>? _rectangles;
|
||||
public IList<Rect>? Rectangles
|
||||
{
|
||||
get => _rectangles;
|
||||
set
|
||||
{
|
||||
_rectangles = value;
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
public SelectionAdorner()
|
||||
{
|
||||
AffectsRender<SelectionAdorner>(FillProperty, StrokeProperty, TransformProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
var rectangles = Rectangles;
|
||||
if (rectangles == null)
|
||||
return;
|
||||
|
||||
using (context.PushTransform(Transform))
|
||||
{
|
||||
Pen pen = new Pen(Stroke, 1);
|
||||
for (int i = 0; i < rectangles.Count; i++)
|
||||
{
|
||||
Rect rectangle = rectangles[i];
|
||||
Rect normalized = rectangle.Width < 0 ? new Rect(rectangle.TopRight, rectangle.BottomLeft) : rectangle;
|
||||
|
||||
if (rectangles[i].Width == 0)
|
||||
context.DrawLine(pen, rectangle.TopLeft, rectangle.BottomRight);
|
||||
else
|
||||
context.DrawRectangle(Fill, pen, normalized);
|
||||
|
||||
RenderCue(context, pen, rectangle.TopLeft, 5, isFilled: true);
|
||||
RenderCue(context, pen, rectangle.TopRight, 5, isFilled: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderCue(DrawingContext context, IPen pen, Point p, double size, bool isFilled)
|
||||
{
|
||||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
|
||||
[
|
||||
new Point(p.X - size / 2, p.Y - size),
|
||||
new Point(p.X + size / 2, p.Y - size),
|
||||
new Point(p.X, p.Y),
|
||||
new Point(p.X - size / 2, p.Y - size),
|
||||
], isFilled));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\build\SampleApp.props" />
|
||||
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
|
||||
<Import Project="..\..\build\BuildTargets.targets" />
|
||||
<Import Project="..\..\build\SourceGenerators.props" />
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -35,6 +35,10 @@ namespace Avalonia.Media
|
||||
/// <summary>
|
||||
/// Gets the trailing length value for the character that got hit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the case of a leading edge, this value is 0. In the case of a trailing edge,
|
||||
/// this value is the number of code points until the next valid caret position.
|
||||
/// </remarks>
|
||||
public int TrailingLength { get; }
|
||||
|
||||
public bool Equals(CharacterHit other)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic implementation of TextParagraphProperties
|
||||
/// Generic implementation of <see cref="TextParagraphProperties"/>.
|
||||
/// </summary>
|
||||
public sealed class GenericTextParagraphProperties : TextParagraphProperties
|
||||
{
|
||||
@@ -11,45 +11,45 @@
|
||||
private double _lineHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Constructing TextParagraphProperties
|
||||
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
|
||||
/// </summary>
|
||||
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
|
||||
/// <param name="textAlignment">logical horizontal alignment</param>
|
||||
/// <param name="textWrap">text wrap option</param>
|
||||
/// <param name="lineHeight">Paragraph line height</param>
|
||||
/// <param name="letterSpacing">letter spacing</param>
|
||||
/// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
|
||||
/// <param name="textAlignment">The alignment of inline content in a block.</param>
|
||||
/// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
|
||||
/// <param name="lineHeight">Paragraph's line spacing.</param>
|
||||
/// <param name="letterSpacing">The amount of letter spacing.</param>
|
||||
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
|
||||
TextAlignment textAlignment = TextAlignment.Left,
|
||||
TextWrapping textWrap = TextWrapping.NoWrap,
|
||||
TextWrapping textWrapping = TextWrapping.NoWrap,
|
||||
double lineHeight = 0,
|
||||
double letterSpacing = 0)
|
||||
{
|
||||
DefaultTextRunProperties = defaultTextRunProperties;
|
||||
_textAlignment = textAlignment;
|
||||
_textWrap = textWrap;
|
||||
_textWrap = textWrapping;
|
||||
_lineHeight = lineHeight;
|
||||
LetterSpacing = letterSpacing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructing TextParagraphProperties
|
||||
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
|
||||
/// </summary>
|
||||
/// <param name="flowDirection">text flow direction</param>
|
||||
/// <param name="textAlignment">logical horizontal alignment</param>
|
||||
/// <param name="firstLineInParagraph">true if the paragraph is the first line in the paragraph</param>
|
||||
/// <param name="alwaysCollapsible">true if the line is always collapsible</param>
|
||||
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
|
||||
/// <param name="textWrap">text wrap option</param>
|
||||
/// <param name="lineHeight">Paragraph line height</param>
|
||||
/// <param name="indent">line indentation</param>
|
||||
/// <param name="letterSpacing">letter spacing</param>
|
||||
/// <param name="flowDirection">The primary text advance direction.</param>
|
||||
/// <param name="textAlignment">The alignment of inline content in a block.</param>
|
||||
/// <param name="firstLineInParagraph"><see langword="true"/> if the paragraph is the first line in the paragraph</param>
|
||||
/// <param name="alwaysCollapsible"><see langword="true"/> if the formatted line may always be collapsed. If <see langword="false"/> (the default), only lines that overflow the paragraph width are collapsed.</param>
|
||||
/// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
|
||||
/// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
|
||||
/// <param name="lineHeight">Paragraph's line spacing.</param>
|
||||
/// <param name="indent">The amount of line indentation.</param>
|
||||
/// <param name="letterSpacing">The amount of letter spacing.</param>
|
||||
public GenericTextParagraphProperties(
|
||||
FlowDirection flowDirection,
|
||||
TextAlignment textAlignment,
|
||||
bool firstLineInParagraph,
|
||||
bool alwaysCollapsible,
|
||||
TextRunProperties defaultTextRunProperties,
|
||||
TextWrapping textWrap,
|
||||
TextWrapping textWrapping,
|
||||
double lineHeight,
|
||||
double indent,
|
||||
double letterSpacing)
|
||||
@@ -59,16 +59,16 @@
|
||||
FirstLineInParagraph = firstLineInParagraph;
|
||||
AlwaysCollapsible = alwaysCollapsible;
|
||||
DefaultTextRunProperties = defaultTextRunProperties;
|
||||
_textWrap = textWrap;
|
||||
_textWrap = textWrapping;
|
||||
_lineHeight = lineHeight;
|
||||
LetterSpacing = letterSpacing;
|
||||
Indent = indent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructing TextParagraphProperties from another one
|
||||
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/> with values copied from the specified <see cref="TextParagraphProperties"/>.
|
||||
/// </summary>
|
||||
/// <param name="textParagraphProperties">source line props</param>
|
||||
/// <param name="textParagraphProperties">The <see cref="TextParagraphProperties"/> to copy values from.</param>
|
||||
public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
|
||||
: this(textParagraphProperties.FlowDirection,
|
||||
textParagraphProperties.TextAlignment,
|
||||
@@ -82,64 +82,43 @@
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This property specifies whether the primary text advance
|
||||
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override FlowDirection FlowDirection
|
||||
{
|
||||
get { return _flowDirection; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This property describes how inline content of a block is aligned.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override TextAlignment TextAlignment
|
||||
{
|
||||
get { return _textAlignment; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paragraph's line height
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override double LineHeight
|
||||
{
|
||||
get { return _lineHeight; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the first line of the paragraph.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override bool FirstLineInParagraph { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the formatted line may always be collapsed. If false (the default),
|
||||
/// only lines that overflow the paragraph width are collapsed.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override bool AlwaysCollapsible { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Paragraph's default run properties
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override TextRunProperties DefaultTextRunProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This property controls whether or not text wraps when it reaches the flow edge
|
||||
/// of its containing block box
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override TextWrapping TextWrapping
|
||||
{
|
||||
get { return _textWrap; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line indentation
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override double Indent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The letter spacing
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override double LetterSpacing { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,63 +6,68 @@
|
||||
public abstract class TextParagraphProperties
|
||||
{
|
||||
/// <summary>
|
||||
/// This property specifies whether the primary text advance
|
||||
/// direction shall be left-to-right, right-to-left.
|
||||
/// Gets a value that specifies whether the primary text advance direction shall be left-to-right, or right-to-left.
|
||||
/// </summary>
|
||||
public abstract FlowDirection FlowDirection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text alignment.
|
||||
/// Gets a value that describes how an inline content of a block is aligned.
|
||||
/// </summary>
|
||||
public abstract TextAlignment TextAlignment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Paragraph's line height
|
||||
/// Gets the height of a line of text.
|
||||
/// </summary>
|
||||
public abstract double LineHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Paragraph's line spacing
|
||||
/// Gets or sets paragraph's line spacing.
|
||||
/// </summary>
|
||||
internal double LineSpacing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the first line of the paragraph.
|
||||
/// Gets a value that indicates whether the text run is the first line of the paragraph.
|
||||
/// </summary>
|
||||
public abstract bool FirstLineInParagraph { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether a formatted line can always be collapsed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If true, the formatted line may always be collapsed. If false (the default),
|
||||
/// only lines that overflow the paragraph width are collapsed.
|
||||
/// </summary>
|
||||
/// </remarks>
|
||||
public virtual bool AlwaysCollapsible
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default text style.
|
||||
/// Gets the default text run properties, such as typeface or foreground brush.
|
||||
/// </summary>
|
||||
public abstract TextRunProperties DefaultTextRunProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of TextDecoration objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If not null, text decorations to apply to all runs in the line. This is in addition
|
||||
/// to any text decorations specified by the TextRunProperties for individual text runs.
|
||||
/// </summary>
|
||||
public virtual TextDecorationCollection? TextDecorations => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text wrapping.
|
||||
/// Gets a value that controls whether text wraps when it reaches the flow edge of its containing block box.
|
||||
/// </summary>
|
||||
public abstract TextWrapping TextWrapping { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Line indentation
|
||||
/// Gets the amount of line indentation.
|
||||
/// </summary>
|
||||
public abstract double Indent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the paragraph indentation.
|
||||
/// Gets the paragraph indentation.
|
||||
/// </summary>
|
||||
public virtual double ParagraphIndent
|
||||
{
|
||||
@@ -75,7 +80,7 @@
|
||||
public virtual double DefaultIncrementalTab => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the letter spacing.
|
||||
/// Gets the amount of letter spacing.
|
||||
/// </summary>
|
||||
public virtual double LetterSpacing { get; }
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow);
|
||||
|
||||
var textSource = new SingleBufferTextSource("ABCDEFHFFHFJHKHFK", defaultProperties, true);
|
||||
|
||||
@@ -488,7 +488,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
var textLine =
|
||||
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap));
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
@@ -544,7 +544,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var textSource = new SingleBufferTextSource(text, defaultProperties);
|
||||
|
||||
@@ -574,7 +574,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
const string text = "012345";
|
||||
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
|
||||
var textSource = new SingleBufferTextSource(text, defaultProperties);
|
||||
var formatter = new TextFormatterImpl();
|
||||
|
||||
@@ -632,7 +632,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
var textLine =
|
||||
formatter.FormatLine(textSource, currentPosition, 300,
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow));
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
@@ -712,7 +712,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
|
||||
var paragraphProperties =
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var textSource = new SingleBufferTextSource(text, defaultProperties);
|
||||
var formatter = new TextFormatterImpl();
|
||||
@@ -742,7 +742,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
using (Start())
|
||||
{
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
|
||||
var formatter = new TextFormatterImpl();
|
||||
@@ -773,7 +773,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
|
||||
var paragraphProperties =
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
|
||||
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.NoWrap);
|
||||
|
||||
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
|
||||
|
||||
@@ -878,7 +878,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
using (Start())
|
||||
{
|
||||
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var text = "Hello World";
|
||||
|
||||
@@ -975,7 +975,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
|
||||
textWrap: wrapping);
|
||||
textWrapping: wrapping);
|
||||
|
||||
using (Start())
|
||||
{
|
||||
@@ -993,7 +993,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
|
||||
textWrap: wrapping);
|
||||
textWrapping: wrapping);
|
||||
|
||||
using (Start())
|
||||
{
|
||||
@@ -1077,7 +1077,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
using (Start())
|
||||
{
|
||||
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var text = "一二三四 TEXT 一二三四五六七八九十零";
|
||||
|
||||
|
||||
@@ -1168,7 +1168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
var defaultProperties =
|
||||
new GenericTextRunProperties(Typeface.Default, 72, foregroundBrush: Brushes.Black);
|
||||
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
|
||||
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
|
||||
|
||||
var textLayout = new TextLayout(new SingleBufferTextSource("01", defaultProperties, true), paragraphProperties, maxWidth: 36);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user