Browse Source
* 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>pull/19469/head
committed by
GitHub
17 changed files with 1440 additions and 78 deletions
@ -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> |
|||
Loading…
Reference in new issue