Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] New unified TextLayout #1950

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions samples/ControlCatalog/Pages/ScreenPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,24 @@ public override void Render(DrawingContext context)

FormattedText text = new FormattedText()
{
Typeface = Typeface.Default
Typeface = Typeface.Default,
Foreground = Brushes.Black
};

text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text);
context.DrawText(text, boundsRect.Position.WithY(boundsRect.Size.Height));

text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
context.DrawText(text, boundsRect.Position.WithY(boundsRect.Size.Height + 20));

text.Text = $"Scaling: {screen.PixelDensity * 100}%";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
context.DrawText(text, boundsRect.Position.WithY(boundsRect.Size.Height + 40));

text.Text = $"Primary: {screen.Primary}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
context.DrawText(text, boundsRect.Position.WithY(boundsRect.Size.Height + 60));

text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
context.DrawText(text, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
}

context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10));
Expand Down
2 changes: 1 addition & 1 deletion samples/RenderDemo/Pages/CustomSkiaPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void Render(IDrawingContextImpl context)
{
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl);
_noSkia.TextLayout.Draw (context, Brushes.Black, new Point());
else
{
canvas.Save();
Expand Down
8 changes: 5 additions & 3 deletions src/Avalonia.Controls/Presenters/TextPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public int SelectionEnd
public int GetCaretIndex(Point point)
{
var hit = FormattedText.HitTestPoint(point);
return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
return hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
}

public override void Render(DrawingContext context)
Expand All @@ -145,6 +145,8 @@ public override void Render(DrawingContext context)

if (selectionStart != selectionEnd)
{
var selectionBrush = SelectionBrush;

var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;

Expand All @@ -156,7 +158,7 @@ public override void Render(DrawingContext context)

foreach (var rect in rects)
{
context.FillRectangle(SelectionBrush, rect);
context.FillRectangle(selectionBrush, rect);
}
}

Expand Down Expand Up @@ -277,7 +279,7 @@ protected override FormattedText CreateFormattedText(Size constraint, string tex
{
result.Spans = new[]
{
new FormattedTextStyleSpan(start, length, SelectionForegroundBrush),
new FormattedTextStyleSpan(start, length, foreground: SelectionForegroundBrush),
};
}

Expand Down
23 changes: 21 additions & 2 deletions src/Avalonia.Controls/TextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ public class TextBlock : Control
public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));

/// <summary>
/// Defines the <see cref="TextTrimming"/> property.
/// </summary>
public static readonly StyledProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming));

private string _text;
private FormattedText _formattedText;
private Size _constraint;
Expand All @@ -109,7 +115,8 @@ static TextBlock()
TextAlignmentProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed
FontWeightProperty.Changed,
TextTrimmingProperty.Changed
).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged());
}

Expand Down Expand Up @@ -219,6 +226,15 @@ public TextAlignment TextAlignment
set { SetValue(TextAlignmentProperty, value); }
}

/// <summary>
/// Gets or sets the text trimming.
/// </summary>
public TextTrimming TextTrimming
{
get { return GetValue(TextTrimmingProperty); }
set { SetValue(TextTrimmingProperty, value); }
}

/// <summary>
/// Gets the value of the attached <see cref="FontFamilyProperty"/> on a control.
/// </summary>
Expand Down Expand Up @@ -338,7 +354,8 @@ public override void Render(DrawingContext context)
}

FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText);

context.DrawText(FormattedText, new Point());
}

/// <summary>
Expand All @@ -354,9 +371,11 @@ protected virtual FormattedText CreateFormattedText(Size constraint, string text
Constraint = constraint,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Foreground = Foreground,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
TextWrapping = TextWrapping,
TextTrimming = TextTrimming
};
}

Expand Down
122 changes: 42 additions & 80 deletions src/Avalonia.Controls/TextBox.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.

using Avalonia.Input.Platform;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Data;
using Avalonia.Utilities;
using Avalonia.Media.Text;

namespace Avalonia.Controls
{
Expand Down Expand Up @@ -110,6 +112,8 @@ public UndoRedoState(string text, int caretPosition)
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" };

private CharacterHit _characterHit;

static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
Expand Down Expand Up @@ -250,9 +254,9 @@ public string Text
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0);
CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0);

if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
{
Expand Down Expand Up @@ -566,9 +570,12 @@ protected override void OnKeyDown(KeyEventArgs e)
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
// a /r should also be deleted.
if (CaretIndex > 1 &&
text[CaretIndex - 1] == '\n' &&
text[CaretIndex - 2] == '\r')
if (CaretIndex > 1 && text[CaretIndex - 1] == '\n' && text[CaretIndex - 2] == '\r')
{
removedCharacters = 2;
}

if (caretIndex >= 2 && char.IsSurrogatePair(text[caretIndex - 2], text[caretIndex - 1]))
{
removedCharacters = 2;
}
Expand All @@ -590,16 +597,7 @@ protected override void OnKeyDown(KeyEventArgs e)

if (!DeleteSelection() && caretIndex < text.Length)
{
var removedCharacters = 1;
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
// a /r should also be deleted.
if (CaretIndex < text.Length - 1 &&
text[caretIndex + 1] == '\n' &&
text[caretIndex] == '\r')
{
removedCharacters = 2;
}
var removedCharacters = _characterHit.TrailingLength;

SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
Expand Down Expand Up @@ -654,7 +652,13 @@ protected override void OnKeyDown(KeyEventArgs e)
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);

var hitTestResult = _presenter.FormattedText.HitTestPoint(point);

_characterHit = hitTestResult.CharacterHit;

var index = CaretIndex = _characterHit.FirstCharacterIndex + _characterHit.TrailingLength;

var text = Text;

if (text != null && e.MouseButton == MouseButton.Left)
Expand Down Expand Up @@ -710,55 +714,21 @@ protected override void UpdateDataValidation(AvaloniaProperty property, BindingN
}
}

private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text);
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0);

private int CoerceCaretIndex(int value, string text)
private int CoerceCaretIndex(int value, int length)
{
if (text == null)
{
return 0;
}
var length = text.Length;

if (value < 0)
{
return 0;
}
else if (value > length)
{
return length;
}
else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n')
{
return value + 1;
}
else
{
return value;
}
}

private int DeleteCharacter(int index)
{
var start = index + 1;
var text = Text;
var c = text[index];
var result = 1;

if (c == '\n' && index > 0 && text[index - 1] == '\r')
{
--index;
++result;
}
else if (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n')
if (value > length)
{
++start;
++result;
return length;
}

Text = text.Substring(0, index) + text.Substring(start);

return result;
return value;
}

private void MoveHorizontal(int direction, bool wholeWord)
Expand All @@ -780,16 +750,11 @@ private void MoveHorizontal(int direction, bool wholeWord)
return;
}

var c = text[index];
var rect = _presenter.FormattedText.HitTestTextPosition(index);

if (direction > 0)
{
CaretIndex += (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') ? 2 : 1;
}
else
{
CaretIndex -= (c == '\n' && index > 0 && text[index - 1] == '\r') ? 2 : 1;
}
var hitTestResult = _presenter.FormattedText.HitTestPoint(new Point(rect.X, rect.Y));

CaretIndex = hitTestResult.CharacterHit.FirstCharacterIndex;
}
else
{
Expand All @@ -807,7 +772,7 @@ private void MoveHorizontal(int direction, bool wholeWord)
private bool MoveVertical(int count)
{
var formattedText = _presenter.FormattedText;
var lines = formattedText.GetLines().ToList();
var lines = formattedText.TextLayout.TextLines;
var caretIndex = CaretIndex;
var lineIndex = GetLine(caretIndex, lines) + count;

Expand All @@ -816,15 +781,12 @@ private bool MoveVertical(int count)
var line = lines[lineIndex];
var rect = formattedText.HitTestTextPosition(caretIndex);
var y = count < 0 ? rect.Y : rect.Bottom;
var point = new Point(rect.X, y + (count * (line.Height / 2)));
var point = new Point(rect.X, y + (count * (line.LineMetrics.Size.Height / 2)));
var hit = formattedText.HitTestPoint(point);
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
CaretIndex = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
return true;
}
else
{
return false;
}
return false;
}

private void MoveHome(bool document)
Expand All @@ -838,17 +800,17 @@ private void MoveHome(bool document)
}
else
{
var lines = _presenter.FormattedText.GetLines();
var lines = _presenter.FormattedText.TextLayout.TextLines;
var pos = 0;

foreach (var line in lines)
{
if (pos + line.Length > caretIndex || pos + line.Length == text.Length)
if (pos + line.Text.Length > caretIndex || pos + line.Text.Length == text.Length)
{
break;
}

pos += line.Length;
pos += line.Text.Length;
}

caretIndex = pos;
Expand All @@ -868,12 +830,12 @@ private void MoveEnd(bool document)
}
else
{
var lines = _presenter.FormattedText.GetLines();
var lines = _presenter.FormattedText.TextLayout.TextLines;
var pos = 0;

foreach (var line in lines)
{
pos += line.Length;
pos += line.Text.Length;

if (pos > caretIndex)
{
Expand Down Expand Up @@ -945,15 +907,15 @@ private string GetSelection()
return text.Substring(start, end - start);
}

private int GetLine(int caretIndex, IList<FormattedTextLine> lines)
private int GetLine(int caretIndex, IReadOnlyList<TextLine> lines)
{
int pos = 0;
int i;

for (i = 0; i < lines.Count - 1; ++i)
{
var line = lines[i];
pos += line.Length;
pos += line.Text.Length;

if (pos > caretIndex)
{
Expand Down
Binary file added src/Avalonia.Visuals/Assets/BidiData.trie
Binary file not shown.
Binary file added src/Avalonia.Visuals/Assets/LineBreakClasses.trie
Binary file not shown.
Loading