Skip to content
Draft
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
22 changes: 21 additions & 1 deletion src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,27 @@ public Rect HitTestTextPosition(int textPosition)

var endX = textLine.GetDistanceFromCharacterHit(nextCharacterHit);

return new Rect(startX, currentY, endX - startX, textLine.Height);
var width = endX - startX;
var adjustedX = startX;

var lineContentLength = textLine.Length - textLine.NewLineLength;

if (lineContentLength > 0)
{
if (textPosition == textLine.FirstTextSourceIndex && textLine.OverhangLeading < 0 && startX > 0)
{
adjustedX += textLine.OverhangLeading;
width -= textLine.OverhangLeading;
}

var lastCharacterPosition = textLine.FirstTextSourceIndex + lineContentLength - 1;
if (textPosition >= lastCharacterPosition && textLine.OverhangTrailing < 0)
{
width -= textLine.OverhangTrailing;
}
}

return new Rect(adjustedX, currentY, width, textLine.Height);
}

return new Rect();
Expand Down
49 changes: 37 additions & 12 deletions src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,7 +1328,7 @@ private TextLineMetrics CreateLineMetrics()
}
}

var inkBounds = new Rect();
Rect? inkBounds = null;

for (var index = 0; index < _textRuns.Length; index++)
{
Expand All @@ -1342,7 +1342,14 @@ private TextLineMetrics CreateLineMetrics()

var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));

inkBounds = inkBounds.Union(runBounds);
if (inkBounds == null)
{
inkBounds = runBounds;
}
else
{
inkBounds = inkBounds.Value.Union(runBounds);
}

widthIncludingWhitespace += textRun.Size.Width;

Expand All @@ -1354,14 +1361,25 @@ private TextLineMetrics CreateLineMetrics()
//Align the bounds at the common baseline
var offsetY = -ascent - drawableTextRun.Baseline;

inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
var drawableBounds = new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size);

if (inkBounds == null)
{
inkBounds = drawableBounds;
}
else
{
inkBounds = inkBounds.Value.Union(drawableBounds);
}

widthIncludingWhitespace += drawableTextRun.Size.Width;

break;
}
}
}

var finalInkBounds = inkBounds ?? new Rect();

var halfLineGap = lineGap * 0.5;
var naturalHeight = descent - ascent + lineGap;
Expand Down Expand Up @@ -1416,18 +1434,18 @@ private TextLineMetrics CreateLineMetrics()
}
}

var extent = inkBounds.Height;
var extent = finalInkBounds.Height;
//The height of overhanging pixels at the bottom
var overhangAfter = inkBounds.Bottom - height + halfLineGap;
var overhangAfter = finalInkBounds.Bottom - height + halfLineGap;
//The width of overhanging pixels at the natural alignment point. Positive value means we are inside.
var overhangLeading = inkBounds.Left;
var overhangLeading = finalInkBounds.Left;
//The width of overhanging pixels at the end of the natural bounds. Positive value means we are inside.
var overhangTrailing = widthIncludingWhitespace - inkBounds.Right;
var hasOverflowed = MathUtilities.GreaterThan(width, _paragraphWidth);
var overhangTrailing = widthIncludingWhitespace - finalInkBounds.Right;
var hasOverflowed = width > _paragraphWidth;

var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var start = GetParagraphOffsetX(width, widthIncludingWhitespace, overhangLeading, overhangTrailing);

_inkBounds = inkBounds.Translate(new Vector(start, 0));
_inkBounds = finalInkBounds.Translate(new Vector(start, 0));

_bounds = new Rect(start, 0, widthIncludingWhitespace, height);

Expand All @@ -1453,9 +1471,11 @@ private TextLineMetrics CreateLineMetrics()
/// </summary>
/// <param name="width">The line width.</param>
/// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
/// <param name="overhangLeading">The leading overhang.</param>
/// <param name="overhangTrailing">The trailing overhang.</param>

/// <returns>The paragraph offset.</returns>
private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace)
private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace, double overhangLeading, double overhangTrailing)
{
if (double.IsPositiveInfinity(_paragraphWidth))
{
Expand Down Expand Up @@ -1492,6 +1512,7 @@ private double GetParagraphOffsetX(double width, double widthIncludingTrailingWh
switch (textAlignment)
{
case TextAlignment.Center:
{
var start = (_paragraphWidth - width) / 2;

if (paragraphFlowDirection == FlowDirection.RightToLeft)
Expand All @@ -1500,8 +1521,12 @@ private double GetParagraphOffsetX(double width, double widthIncludingTrailingWh
}

return Math.Max(0, start);
}
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
{
var overhangAdjustment = Math.Min(0, overhangTrailing);
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace + overhangAdjustment);
}
default:
return 0;
}
Expand Down
156 changes: 145 additions & 11 deletions src/Avalonia.Controls/Presenters/TextPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,41 @@ private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typef
return textLayout;
}

private double GetLineOffsetX(TextLine line, double controlWidth, TextAlignment textAlignment)
{
var lineInkStartOffset = line.OverhangLeading;
var lineTrailingOverhang = -Math.Min(0, line.OverhangTrailing);
var lineLeadingOverhang = -Math.Min(0, line.OverhangLeading);
var lineInkWidth = line.WidthIncludingTrailingWhitespace + lineLeadingOverhang + lineTrailingOverhang;

switch (textAlignment)
{
case TextAlignment.Center:
return (controlWidth - lineInkWidth) / 2 - lineInkStartOffset - line.Start;

case TextAlignment.Right:
return controlWidth - line.WidthIncludingTrailingWhitespace - lineTrailingOverhang - line.Start;

default: // Left, Justify
return -lineInkStartOffset - line.Start;
}
}

private TextAlignment GetResolvedTextAlignment()
{
var textAlignment = TextAlignment;
var flowDirection = FlowDirection;

if (textAlignment == TextAlignment.Start)
return flowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
else if (textAlignment == TextAlignment.End)
return flowDirection == FlowDirection.RightToLeft ? TextAlignment.Left : TextAlignment.Right;
else if (textAlignment == TextAlignment.DetectFromContent)
return TextAlignment.Left;

return textAlignment;
}

/// <summary>
/// Renders the <see cref="TextPresenter"/> to a drawing context.
/// </summary>
Expand All @@ -383,8 +418,12 @@ private void RenderInternal(DrawingContext context)
context.FillRectangle(background, new Rect(Bounds.Size));
}

if (TextLayout.TextLines.Count == 0)
{
return;
}

var top = 0d;
var left = 0.0;

var textHeight = TextLayout.Height;

Expand All @@ -402,7 +441,17 @@ private void RenderInternal(DrawingContext context)
}
}

TextLayout.Draw(context, new Point(left, top));
var controlWidth = Bounds.Width;
var textAlignment = GetResolvedTextAlignment();

var currentY = top;

foreach (var line in TextLayout.TextLines)
{
var offsetX = GetLineOffsetX(line, controlWidth, textAlignment);
line.Draw(context, new Point(offsetX, currentY));
currentY += line.Height;
}
}

public sealed override void Render(DrawingContext context)
Expand All @@ -417,10 +466,35 @@ public sealed override void Render(DrawingContext context)
var length = Math.Max(selectionStart, selectionEnd) - start;

var rects = TextLayout.HitTestTextRange(start, length);

var controlWidth = Bounds.Width;
var textAlignment = GetResolvedTextAlignment();

foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
var currentY = 0d;
TextLine? targetLine = null;

foreach (var line in TextLayout.TextLines)
{
if (currentY + line.Height > rect.Y)
{
targetLine = line;
break;
}
currentY += line.Height;
}

if (targetLine != null)
{
var offsetX = GetLineOffsetX(targetLine, controlWidth, textAlignment);
var transformedRect = rect.WithX(rect.X + offsetX);
context.FillRectangle(selectionBrush, PixelRect.FromRect(transformedRect, 1).ToRect(1));
}
else
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
}

Expand Down Expand Up @@ -472,11 +546,18 @@ public sealed override void Render(DrawingContext context)
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];

var x = Math.Floor(_caretBounds.X) + 0.5;
var caretX = Math.Max(0, _caretBounds.X);

var x = Math.Floor(caretX) + 0.5;
var y = Math.Floor(_caretBounds.Y) + 0.5;
var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;

if (_caretBounds.X > 0 && _caretBounds.X >= textLine.WidthIncludingTrailingWhitespace)
var controlWidth = Bounds.Width;
var textAlignment = GetResolvedTextAlignment();
var offsetX = GetLineOffsetX(textLine, controlWidth, textAlignment);
var lineEndX = textLine.WidthIncludingTrailingWhitespace + offsetX + textLine.Start;

if (caretX > 0 && caretX >= lineEndX)
{
x -= 1;
}
Expand Down Expand Up @@ -646,17 +727,34 @@ protected override Size MeasureOverride(Size availableSize)

InvalidateArrange();

// The textWidth used here is matching that TextBlock uses to measure the text.
var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
var maxLeadingOverhang = 0.0;
var maxTrailingOverhang = 0.0;

foreach (var line in TextLayout.TextLines)
{
maxLeadingOverhang = Math.Max(maxLeadingOverhang, -Math.Min(0, line.OverhangLeading));
maxTrailingOverhang = Math.Max(maxTrailingOverhang, -Math.Min(0, line.OverhangTrailing));
}

var textWidth = TextLayout.WidthIncludingTrailingWhitespace + maxLeadingOverhang + maxTrailingOverhang;

return new Size(textWidth, TextLayout.Height);
}

protected override Size ArrangeOverride(Size finalSize)
{
var finalWidth = finalSize.Width;

var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
textWidth = Math.Ceiling(textWidth);
var maxLeadingOverhang = 0.0;
var maxTrailingOverhang = 0.0;

foreach (var line in TextLayout.TextLines)
{
maxLeadingOverhang = Math.Max(maxLeadingOverhang, -Math.Min(0, line.OverhangLeading));
maxTrailingOverhang = Math.Max(maxTrailingOverhang, -Math.Min(0, line.OverhangTrailing));
}

var textWidth = Math.Ceiling(TextLayout.WidthIncludingTrailingWhitespace + maxLeadingOverhang + maxTrailingOverhang);

if (finalSize.Width < textWidth)
{
Expand Down Expand Up @@ -715,14 +813,46 @@ public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)

public void MoveCaretToPoint(Point point)
{
var hit = TextLayout.HitTestPoint(point);
var transformedPoint = TransformPointToTextLayout(point);

var hit = TextLayout.HitTestPoint(transformedPoint);

UpdateCaret(hit.CharacterHit);

_navigationPosition = _caretBounds.Position;

CaretChanged();
}

private Point TransformPointToTextLayout(Point point)
{
if (TextLayout.TextLines.Count == 0)
{
return point;
}

var controlWidth = Bounds.Width;
var textAlignment = GetResolvedTextAlignment();

var currentY = 0d;
TextLine? targetLine = null;

foreach (var line in TextLayout.TextLines)
{
if (currentY + line.Height > point.Y)
{
targetLine = line;
break;
}
currentY += line.Height;
}

targetLine ??= TextLayout.TextLines[TextLayout.TextLines.Count - 1];

var offsetX = GetLineOffsetX(targetLine, controlWidth, textAlignment);

return new Point(point.X - offsetX, point.Y);
}

public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
{
Expand Down Expand Up @@ -917,7 +1047,11 @@ internal void UpdateCaret(CharacterHit characterHit, bool notify = true)
distanceY += currentLine.Height;
}

var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
var controlWidth = Bounds.Width;
var textAlignment = GetResolvedTextAlignment();
var offsetX = GetLineOffsetX(textLine, controlWidth, textAlignment);

var caretBounds = new Rect(distanceX + offsetX, distanceY, 0, textLine.Height);

if (caretBounds != _caretBounds)
{
Expand Down
Loading
Loading