Browse Source

Merge branch 'master' into feature/DataGridHeaderTemplate

pull/7080/head
Max Katz 5 years ago
committed by GitHub
parent
commit
ef3eb31c7b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/ISSUE_TEMPLATE/config.yml
  2. 2
      CONTRIBUTING.md
  3. 1
      readme.md
  4. 19
      src/Avalonia.Base/Collections/AvaloniaList.cs
  5. 74
      src/Avalonia.Controls/Border.cs
  6. 55
      src/Avalonia.Controls/TextBox.cs
  7. 14
      src/Avalonia.Controls/TreeView.cs
  8. 8
      src/Avalonia.Controls/TreeViewItem.cs
  9. 52
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  10. 169
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  11. 152
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  12. 48
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  13. 46
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

4
.github/ISSUE_TEMPLATE/config.yml

@ -3,6 +3,6 @@ contact_links:
- name: Questions, Discussions, Ideas
url: https://github.com/AvaloniaUI/Avalonia/discussions/new
about: Please ask and answer questions here.
- name: Avalonia Community Support on Gitter
url: https://gitter.im/AvaloniaUI/Avalonia
- name: Avalonia Community Support on Telegram
url: https://t.me/Avalonia
about: Please ask and answer questions here.

2
CONTRIBUTING.md

@ -2,7 +2,7 @@
## Before You Start
Drop into our [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature.
Drop into our [telegram group](https://t.me/Avalonia) or [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature.
## Style

1
readme.md

@ -1,3 +1,4 @@
[![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)

19
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -503,11 +503,24 @@ namespace Avalonia.Collections
{
Contract.Requires<ArgumentNullException>(items != null);
foreach (var i in items)
var hItems = new HashSet<T>(items);
int counter = 0;
for (int i = _inner.Count - 1; i >= 0; --i)
{
// TODO: Optimize to only send as many notifications as necessary.
Remove(i);
if (hItems.Contains(_inner[i]))
{
counter += 1;
}
else if(counter > 0)
{
RemoveRange(i + 1, counter);
counter = 0;
}
}
if (counter > 0)
RemoveRange(0, counter);
}
/// <summary>

74
src/Avalonia.Controls/Border.cs

@ -1,3 +1,5 @@
using Avalonia.Collections;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;
@ -41,7 +43,31 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
/// <summary>
/// Defines the <see cref="BorderDashOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> BorderDashOffsetProperty =
AvaloniaProperty.Register<Border, double>(nameof(BorderDashOffset));
/// <summary>
/// Defines the <see cref="BorderDashArray"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> BorderDashArrayProperty =
AvaloniaProperty.Register<Border, AvaloniaList<double>?>(nameof(BorderDashArray));
/// <summary>
/// Defines the <see cref="BorderLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> BorderLineCapProperty =
AvaloniaProperty.Register<Border, PenLineCap>(nameof(BorderLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="BorderLineJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> BorderLineJoinProperty =
AvaloniaProperty.Register<Border, PenLineJoin>(nameof(BorderLineJoin), PenLineJoin.Miter);
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -54,6 +80,10 @@ namespace Avalonia.Controls
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
BorderDashArrayProperty,
BorderLineCapProperty,
BorderLineJoinProperty,
BorderDashOffsetProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -76,6 +106,15 @@ namespace Avalonia.Controls
set { SetValue(BorderBrushProperty, value); }
}
/// <summary>
/// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps that is used to outline shapes.
/// </summary>
public AvaloniaList<double>? BorderDashArray
{
get { return GetValue(BorderDashArrayProperty); }
set { SetValue(BorderDashArrayProperty, value); }
}
/// <summary>
/// Gets or sets the thickness of the border.
/// </summary>
@ -85,6 +124,33 @@ namespace Avalonia.Controls
set { SetValue(BorderThicknessProperty, value); }
}
/// <summary>
/// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
/// </summary>
public double BorderDashOffset
{
get { return GetValue(BorderDashOffsetProperty); }
set { SetValue(BorderDashOffsetProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineCap"/> enumeration value that describes the shape at the ends of a line.
/// </summary>
public PenLineCap BorderLineCap
{
get { return GetValue(BorderLineCapProperty); }
set { SetValue(BorderLineCapProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineJoin"/> enumeration value that specifies the type of join that is used at the vertices of a Shape.
/// </summary>
public PenLineJoin BorderLineJoin
{
get { return GetValue(BorderLineJoinProperty); }
set { SetValue(BorderLineJoinProperty, value); }
}
/// <summary>
/// Gets or sets the radius of the border rounded corners.
/// </summary>
@ -93,7 +159,7 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
@ -102,7 +168,7 @@ namespace Avalonia.Controls
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
@ -110,7 +176,7 @@ namespace Avalonia.Controls
public override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
}
/// <summary>

55
src/Avalonia.Controls/TextBox.cs

@ -642,15 +642,31 @@ namespace Avalonia.Controls
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
if (IsUndoEnabled)
var oldText = _text;
_ignoreTextChanges = true;
try
{
_undoRedoHelper.DiscardRedo();
DeleteSelection(false);
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
if (_text != oldText)
{
RaisePropertyChanged(TextProperty, oldText, _text);
}
}
finally
{
_ignoreTextChanges = false;
}
}
}
@ -1285,7 +1301,7 @@ namespace Avalonia.Controls
CaretIndex = SelectionEnd;
}
private bool DeleteSelection()
private bool DeleteSelection(bool raiseTextChanged = true)
{
if (!IsReadOnly)
{
@ -1297,7 +1313,7 @@ namespace Avalonia.Controls
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text;
SetTextInternal(text.Substring(0, start) + text.Substring(end));
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
CaretIndex = start;
ClearSelection();
return true;
@ -1348,16 +1364,23 @@ namespace Avalonia.Controls
return i;
}
private void SetTextInternal(string value)
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
try
if (raiseTextChanged)
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
try
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
}
finally
{
_ignoreTextChanges = false;
}
}
finally
else
{
_ignoreTextChanges = false;
_text = value;
}
}

14
src/Avalonia.Controls/TreeView.cs

@ -392,14 +392,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
var result = new TreeItemContainerGenerator<TreeViewItem>(
var result = CreateTreeItemContainerGenerator();
result.Index.Materialized += ContainerMaterialized;
return result;
}
protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() =>
CreateTreeItemContainerGenerator<TreeViewItem>();
protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>() where TVItem: TreeViewItem, new()
{
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
result.Index.Materialized += ContainerMaterialized;
return result;
}
/// <inheritdoc/>

8
src/Avalonia.Controls/TreeViewItem.cs

@ -92,9 +92,13 @@ namespace Avalonia.Controls
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<TreeViewItem>();
/// <inheritdoc/>
protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>()
where TVItem: TreeViewItem, new()
{
return new TreeItemContainerGenerator<TreeViewItem>(
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,

52
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,8 +1,10 @@
using System;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia.Controls.Utils
{
@ -15,8 +17,13 @@ namespace Avalonia.Controls.Utils
private Size _size;
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private AvaloniaList<double> _borderDashArray;
private double _borderDashOffset;
private PenLineCap _borderLineCap;
private PenLineJoin _borderJoin;
private bool _initialized;
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
@ -60,7 +67,8 @@ namespace Avalonia.Controls.Utils
if (boundRect.Width != 0 && innerRect.Height != 0)
{
var borderGeometryKeypoints = new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);
var borderGeometryKeypoints =
new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);
var borderGeometry = new StreamGeometry();
using (var ctx = borderGeometry.Open())
@ -84,17 +92,22 @@ namespace Avalonia.Controls.Utils
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush background, IBrush borderBrush, BoxShadows boxShadows)
IBrush background, IBrush borderBrush, BoxShadows boxShadows, double borderDashOffset = 0,
PenLineCap borderLineCap = PenLineCap.Flat, PenLineJoin borderLineJoin = PenLineJoin.Miter,
AvaloniaList<double> borderDashArray = null)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows);
RenderCore(context, background, borderBrush, boxShadows, borderDashOffset, borderLineCap, borderLineJoin,
borderDashArray);
}
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows,
double borderDashOffset, PenLineCap borderLineCap, PenLineJoin borderLineJoin,
AvaloniaList<double> borderDashArray)
{
if (_useComplexRendering)
{
@ -115,11 +128,25 @@ namespace Avalonia.Controls.Utils
var borderThickness = _borderThickness.Top;
IPen pen = null;
ImmutableDashStyle? dashStyle = null;
if (borderDashArray != null && borderDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(borderDashArray, borderDashOffset);
}
if (borderBrush != null && borderThickness > 0)
{
pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness);
pen = new ImmutablePen(
borderBrush.ToImmutable(),
borderThickness,
dashStyle,
borderLineCap,
borderLineJoin);
}
var rect = new Rect(_size);
if (!MathUtilities.IsZero(borderThickness))
rect = rect.Deflate(borderThickness * 0.5);
@ -130,7 +157,8 @@ namespace Avalonia.Controls.Utils
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect,
BorderGeometryKeypoints keypoints)
{
context.BeginFigure(keypoints.TopLeft, true);
@ -184,7 +212,8 @@ namespace Avalonia.Controls.Utils
private class BorderGeometryKeypoints
{
internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius, bool inner)
internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius,
bool inner)
{
var left = 0.5 * borderThickness.Left;
var top = 0.5 * borderThickness.Top;
@ -206,10 +235,13 @@ namespace Avalonia.Controls.Utils
topLeftX = Math.Max(0, cornerRadius.TopLeft - left) + boundRect.TopLeft.X;
topRightX = boundRect.Width - Math.Max(0, cornerRadius.TopRight - top) + boundRect.TopLeft.X;
rightTopY = Math.Max(0, cornerRadius.TopRight - right) + boundRect.TopLeft.Y;
rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) + boundRect.TopLeft.Y;
bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) + boundRect.TopLeft.X;
rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) +
boundRect.TopLeft.Y;
bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) +
boundRect.TopLeft.X;
bottomLeftX = Math.Max(0, cornerRadius.BottomLeft - left) + boundRect.TopLeft.X;
leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) + boundRect.TopLeft.Y;
leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) +
boundRect.TopLeft.Y;
}
else
{

169
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia
@ -13,6 +14,25 @@ namespace Avalonia.Skia
/// </summary>
internal class FormattedTextImpl : IFormattedTextImpl
{
private static readonly ThreadLocal<SKTextBlobBuilder> t_builder = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder());
private const float MAX_LINE_WIDTH = 10000;
private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
new List<KeyValuePair<FBrushRange, IBrush>>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; }
private readonly TextWrapping _wrapping;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0;
private float _lineOffset = 0;
private Rect _bounds;
private List<AvaloniaFormattedTextLine> _skiaLines;
private ReadOnlySlice<ushort> _glyphs;
private ReadOnlySlice<float> _advances;
public FormattedTextImpl(
string text,
Typeface typeface,
@ -23,12 +43,9 @@ namespace Avalonia.Skia
IReadOnlyList<FormattedTextStyleSpan> spans)
{
Text = text ?? string.Empty;
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl;
UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize);
_paint = new SKPaint
{
TextEncoding = SKTextEncoding.Utf16,
@ -37,7 +54,7 @@ namespace Avalonia.Skia
LcdRenderText = true,
SubpixelText = true,
IsLinearText = true,
Typeface = glyphTypeface.Typeface,
Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};
@ -195,6 +212,40 @@ namespace Avalonia.Skia
return Text;
}
private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint)
{
if(length == 0)
{
return;
}
var glyphs = _glyphs.Buffer.Span.Slice(start, length);
var advances = _advances.Buffer.Span.Slice(start, length);
var builder = t_builder.Value;
var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0);
buffer.SetGlyphs(glyphs);
var positions = buffer.GetPositionSpan();
var pos = 0f;
for (int i = 0; i < advances.Length; i++)
{
positions[i] = pos;
pos += advances[i];
}
var blob = builder.Build();
if(blob != null)
{
canvas.DrawText(blob, x, y, paint);
}
}
internal void Draw(DrawingContextImpl context,
SKCanvas canvas,
SKPoint origin,
@ -244,16 +295,15 @@ namespace Avalonia.Skia
if (!hasCusomFGBrushes)
{
var subString = Text.Substring(line.Start, line.Length);
canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint);
}
else
{
float currX = x;
string subStr;
float measure;
int len;
float factor;
switch (paint.TextAlign)
{
case SKTextAlign.Left:
@ -269,8 +319,7 @@ namespace Avalonia.Skia
throw new ArgumentOutOfRangeException();
}
var textLine = Text.Substring(line.Start, line.Length);
currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor;
currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor;
for (int i = line.Start; i < line.Start + line.Length;)
{
@ -288,13 +337,12 @@ namespace Avalonia.Skia
currentWrapper = foreground;
}
subStr = Text.Substring(i, len);
measure = paint.MeasureText(subStr);
measure = MeasureText(i, len);
currX += measure * factor;
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint);
i += len;
currX += measure * (1 - factor);
@ -310,21 +358,6 @@ namespace Avalonia.Skia
}
}
private const float MAX_LINE_WIDTH = 10000;
private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
new List<KeyValuePair<FBrushRange, IBrush>>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; }
private readonly TextWrapping _wrapping;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0;
private float _lineOffset = 0;
private Rect _bounds;
private List<AvaloniaFormattedTextLine> _skiaLines;
private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper,
ref IDisposable curr, SKPaint paint, bool canUseLcdRendering)
{
@ -352,9 +385,8 @@ namespace Avalonia.Skia
}
else
{
float measuredWidth;
string subText = textInput.Substring(textIndex, stop - textIndex);
lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth);
lengthBreak = (int)paint.BreakText(subText, maxWidth, out _);
}
//Check for white space or line breakers before the lengthBreak
@ -468,8 +500,7 @@ namespace Avalonia.Skia
for (int i = line.Start; i < line.Start + line.TextLength; i++)
{
var c = Text[i];
var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString());
var w = line.IsEmptyTrailingLine ? 0 : _advances[i];
_rects.Add(new Rect(
prevRight,
@ -554,7 +585,7 @@ namespace Avalonia.Skia
// This seems like the best measure of full vertical extent
// matches Direct2D line height
_lineHeight = mDescent - mAscent;
_lineHeight = mDescent - mAscent + metrics.Leading;
// Rendering is relative to baseline
_lineOffset = (-metrics.Ascent);
@ -585,7 +616,7 @@ namespace Avalonia.Skia
line.Start = curOff;
line.TextLength = measured;
subString = Text.Substring(line.Start, line.TextLength);
lineWidth = _paint.MeasureText(subString);
lineWidth = MeasureText(line.Start, line.TextLength);
line.Length = measured - trailingnumber;
line.Width = lineWidth;
line.Height = _lineHeight;
@ -608,8 +639,7 @@ namespace Avalonia.Skia
AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine();
lastLine.TextLength = lengthDiff;
lastLine.Start = curOff - lengthDiff;
var lastLineSubString = Text.Substring(line.Start, line.TextLength);
var lastLineWidth = _paint.MeasureText(lastLineSubString);
var lastLineWidth = MeasureText(line.Start, line.TextLength);
lastLine.Length = 0;
lastLine.Width = lastLineWidth;
lastLine.Height = _lineHeight;
@ -668,6 +698,67 @@ namespace Avalonia.Skia
}
}
private float MeasureText(int start, int length)
{
var width = 0f;
for (int i = start; i < start + length; i++)
{
var advance = _advances[i];
width += advance;
}
return width;
}
private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize)
{
var glyphs = new ushort[text.Length];
var advances = new float[text.Length];
var scale = fontSize / glyphTypeface.DesignEmHeight;
var width = 0f;
var characters = text.AsSpan();
for (int i = 0; i < characters.Length; i++)
{
var c = characters[i];
float advance;
ushort glyph;
switch (c)
{
case (char)0:
{
glyph = glyphTypeface.GetGlyph(0x200B);
advance = 0;
break;
}
case '\t':
{
glyph = glyphTypeface.GetGlyph(' ');
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4;
break;
}
default:
{
glyph = glyphTypeface.GetGlyph(c);
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale;
break;
}
}
glyphs[i] = glyph;
advances[i] = advance;
width += advance;
}
_glyphs = new ReadOnlySlice<ushort>(glyphs);
_advances = new ReadOnlySlice<float>(advances);
}
private float TransformX(float originX, float lineWidth, SKTextAlign align)
{
float x = 0;

152
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -357,5 +357,157 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(target, result);
}
[Fact]
public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range_With_Duplicate_Source_Items()
{
var items = Enumerable.Range(0, 20).Select(x => $"Item {x / 2}");
var target = new AvaloniaList<string>(items);
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(10, e.OldStartingIndex);
Assert.Equal(new[]
{
"Item 5",
"Item 5",
"Item 6",
"Item 6",
"Item 7",
"Item 7",
}, e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Send_Multiple_Notifications_For_Non_Sequential_Range()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var raised = 0;
var toRemove = new[]
{
new[] { "Item 2", "Item 3" },
new[] { "Item 5", "Item 6" }
};
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
if (raised == 0)
{
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove[1], e.OldItems);
}
else
{
Assert.Equal(2, e.OldStartingIndex);
Assert.Equal(toRemove[0], e.OldItems);
}
++raised;
};
target.RemoveAll(toRemove[0].Concat(toRemove[1]));
Assert.Equal(2, raised);
}
[Fact]
public void RemoveAll_Should_Send_Multiple_Notifications_For_Sequential_Range_With_Nonsequential_Duplicate_Source_Items()
{
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}");
var target = new AvaloniaList<string>(items.Concat(items));
var raised = 0;
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
if (raised == 0)
{
Assert.Equal(15, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
}
else
{
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
}
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(2, raised);
}
[Fact]
public void RemoveAll_Should_Not_Send_Notification_For_Items_Not_Present()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var toRemove = new[] { "Item 5", "Item 6", "Item 7", "Not present" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove.Take(3).ToArray(), e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Handle_Empty_List()
{
var target = new AvaloniaList<string>();
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(0, raised);
}
}
}

48
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -784,6 +784,54 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Setting_SelectedText_Should_Fire_Single_Text_Changed_Notification()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true,
SelectionStart = 1,
SelectionEnd = 3,
};
var values = new List<string>();
target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x));
target.SelectedText = "A";
Assert.Equal(new[] { "0123", "0A3" }, values);
}
}
[Fact]
public void Entering_Text_With_SelectedText_Should_Fire_Single_Text_Changed_Notification()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true,
SelectionStart = 1,
SelectionEnd = 3,
};
var values = new List<string>();
target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x));
RaiseTextEvent(target, "A");
Assert.Equal(new[] { "0123", "0A3" }, values);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),

46
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@ -24,7 +25,7 @@ namespace Avalonia.Controls.UnitTests
public class TreeViewTests
{
MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Items_Should_Be_Created()
{
@ -675,7 +676,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(node, focus.Current);
}
}
[Fact]
public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree()
{
@ -1166,6 +1167,34 @@ namespace Avalonia.Controls.UnitTests
Assert.Empty(target.ItemContainerGenerator.Index.Containers);
}
[Fact]
public void Can_Use_Derived_TreeViewItem()
{
var tree = CreateTestTreeData();
var target = new DerivedTreeViewWithDerivedTreeViewItems
{
Template = CreateTreeViewTemplate(),
Items = tree,
};
ApplyTemplates(target);
// Verify that all items are DerivedTreeViewItem
VerifyItemType(target.ItemContainerGenerator);
void VerifyItemType(ITreeItemContainerGenerator containerGenerator)
{
foreach (var container in containerGenerator.Index.Containers)
{
var item = Assert.IsType<DerivedTreeViewItem>(container);
if (item.ItemCount > 0)
{
VerifyItemType(item.ItemContainerGenerator);
}
}
}
}
private void ApplyTemplates(TreeView tree)
{
tree.ApplyTemplate();
@ -1376,6 +1405,17 @@ namespace Avalonia.Controls.UnitTests
{
}
private class DerivedTreeViewWithDerivedTreeViewItems : TreeView
{
protected override ITreeItemContainerGenerator CreateTreeItemContainerGenerator() =>
CreateTreeItemContainerGenerator<DerivedTreeViewItem>();
}
private class DerivedTreeViewItem : TreeViewItem
{
protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<DerivedTreeViewItem>();
}
private class TestDataContext : INotifyPropertyChanged
{
private string _selectedItem;
@ -1398,7 +1438,7 @@ namespace Avalonia.Controls.UnitTests
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
}

Loading…
Cancel
Save