diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 687355d825..9e28f532be 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.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.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dcf95ce33c..cb5a7d7897 100644
--- a/CONTRIBUTING.md
+++ b/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
diff --git a/readme.md b/readme.md
index a1cdb6fe9d..a8a6399f2f 100644
--- a/readme.md
+++ b/readme.md
@@ -1,3 +1,4 @@
+[](https://t.me/Avalonia)
[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index 2f1cb2888e..5782159813 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -503,11 +503,24 @@ namespace Avalonia.Collections
{
Contract.Requires(items != null);
- foreach (var i in items)
+ var hItems = new HashSet(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);
}
///
diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs
index 0c6949465b..ee67f303f3 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/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
///
public static readonly StyledProperty BoxShadowProperty =
AvaloniaProperty.Register(nameof(BoxShadow));
-
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty BorderDashOffsetProperty =
+ AvaloniaProperty.Register(nameof(BorderDashOffset));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty?> BorderDashArrayProperty =
+ AvaloniaProperty.Register?>(nameof(BorderDashArray));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty BorderLineCapProperty =
+ AvaloniaProperty.Register(nameof(BorderLineCap), PenLineCap.Flat);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty BorderLineJoinProperty =
+ AvaloniaProperty.Register(nameof(BorderLineJoin), PenLineJoin.Miter);
+
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
///
@@ -54,6 +80,10 @@ namespace Avalonia.Controls
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
+ BorderDashArrayProperty,
+ BorderLineCapProperty,
+ BorderLineJoinProperty,
+ BorderDashOffsetProperty,
BoxShadowProperty);
AffectsMeasure(BorderThicknessProperty);
}
@@ -76,6 +106,15 @@ namespace Avalonia.Controls
set { SetValue(BorderBrushProperty, value); }
}
+ ///
+ /// Gets or sets a collection of values that indicate the pattern of dashes and gaps that is used to outline shapes.
+ ///
+ public AvaloniaList? BorderDashArray
+ {
+ get { return GetValue(BorderDashArrayProperty); }
+ set { SetValue(BorderDashArrayProperty, value); }
+ }
+
///
/// Gets or sets the thickness of the border.
///
@@ -85,6 +124,33 @@ namespace Avalonia.Controls
set { SetValue(BorderThicknessProperty, value); }
}
+ ///
+ /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
+ ///
+ public double BorderDashOffset
+ {
+ get { return GetValue(BorderDashOffsetProperty); }
+ set { SetValue(BorderDashOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a enumeration value that describes the shape at the ends of a line.
+ ///
+ public PenLineCap BorderLineCap
+ {
+ get { return GetValue(BorderLineCapProperty); }
+ set { SetValue(BorderLineCapProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a enumeration value that specifies the type of join that is used at the vertices of a Shape.
+ ///
+ public PenLineJoin BorderLineJoin
+ {
+ get { return GetValue(BorderLineJoinProperty); }
+ set { SetValue(BorderLineJoinProperty, value); }
+ }
+
///
/// Gets or sets the radius of the border rounded corners.
///
@@ -93,7 +159,7 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
-
+
///
/// Gets or sets the box shadow effect parameters
///
@@ -102,7 +168,7 @@ namespace Avalonia.Controls
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
-
+
///
/// Renders the control.
///
@@ -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);
}
///
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 0a29db555c..00fc6002d1 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index 78cd22ae32..03f6fd3aaa 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -392,14 +392,22 @@ namespace Avalonia.Controls
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
- var result = new TreeItemContainerGenerator(
+ var result = CreateTreeItemContainerGenerator();
+ result.Index.Materialized += ContainerMaterialized;
+ return result;
+ }
+
+ protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() =>
+ CreateTreeItemContainerGenerator();
+
+ protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new()
+ {
+ return new TreeItemContainerGenerator(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
- result.Index.Materialized += ContainerMaterialized;
- return result;
}
///
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index 8ce258b546..88fb3cd9f1 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -92,9 +92,13 @@ namespace Avalonia.Controls
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
///
- protected override IItemContainerGenerator CreateItemContainerGenerator()
+ protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator();
+
+ ///
+ protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator()
+ where TVItem: TreeViewItem, new()
{
- return new TreeItemContainerGenerator(
+ return new TreeItemContainerGenerator(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
index f8ab58d46e..bae787f0ed 100644
--- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
+++ b/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 _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()
@@ -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 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 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
{
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 5f4980e461..81a79201d0 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/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
///
internal class FormattedTextImpl : IFormattedTextImpl
{
+ private static readonly ThreadLocal t_builder = new ThreadLocal(() => new SKTextBlobBuilder());
+
+ private const float MAX_LINE_WIDTH = 10000;
+
+ private readonly List> _foregroundBrushes =
+ new List>();
+ private readonly List _lines = new List();
+ private readonly SKPaint _paint;
+ private readonly List _rects = new List();
+ 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 _skiaLines;
+ private ReadOnlySlice _glyphs;
+ private ReadOnlySlice _advances;
+
public FormattedTextImpl(
string text,
Typeface typeface,
@@ -23,12 +43,9 @@ namespace Avalonia.Skia
IReadOnlyList 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> _foregroundBrushes =
- new List>();
- private readonly List _lines = new List();
- private readonly SKPaint _paint;
- private readonly List _rects = new List();
- 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 _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(glyphs);
+ _advances = new ReadOnlySlice(advances);
+ }
+
private float TransformX(float originX, float lineWidth, SKTextAlign align)
{
float x = 0;
diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
index d5ac01a092..f5ae4cc1e0 100644
--- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
+++ b/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(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(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(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(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(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();
+ var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
+ var raised = 0;
+
+ target.CollectionChanged += (s, e) =>
+ {
+ ++raised;
+ };
+
+ target.RemoveAll(toRemove);
+
+ Assert.Equal(0, raised);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index cb20071860..23cae8fd0d 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/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();
+ 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();
+ 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(),
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 72ba3ab273..2169b15cad 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/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(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();
+ }
+
+ private class DerivedTreeViewItem : TreeViewItem
+ {
+ protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator();
+ }
+
private class TestDataContext : INotifyPropertyChanged
{
private string _selectedItem;
@@ -1398,7 +1438,7 @@ namespace Avalonia.Controls.UnitTests
}
public event PropertyChangedEventHandler PropertyChanged;
-
+
}
}
}