Browse Source

Merge branch 'master' into fixes/avalonia-collections-nullability

pull/7082/head
Max Katz 5 years ago
committed by GitHub
parent
commit
47153bffd7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      samples/ControlCatalog/Pages/DataGridPage.xaml
  2. 19
      src/Avalonia.Base/Collections/AvaloniaList.cs
  3. 27
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  4. 3
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  5. 3
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  6. 74
      src/Avalonia.Controls/Border.cs
  7. 55
      src/Avalonia.Controls/TextBox.cs
  8. 52
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  9. 152
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  10. 48
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

9
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -4,6 +4,12 @@
x:Class="ControlCatalog.Pages.DataGridPage"> x:Class="ControlCatalog.Pages.DataGridPage">
<UserControl.Resources> <UserControl.Resources>
<local:GDPValueConverter x:Key="GDPConverter" /> <local:GDPValueConverter x:Key="GDPConverter" />
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader">
<StackPanel Orientation="Horizontal" Spacing="5">
<PathIcon Height="12" Data="M 255 116 A 1 1 0 0 0 254 117 L 254 130 A 1 1 0 0 0 255 131 A 1 1 0 0 0 256 130 L 256 123.87109 C 256.1125 123.90694 256.2187 123.94195 256.33984 123.97852 C 257.18636 124.23404 258.19155 124.5 259 124.5 C 259.80845 124.5 260.52133 124.2168 261.17773 123.9668 C 261.83414 123.7168 262.43408 123.5 263 123.5 C 263.56592 123.5 264.5612 123.73404 265.37109 123.97852 C 266.18098 124.22299 266.82227 124.4668 266.82227 124.4668 A 0.50005 0.50005 0 0 0 267.5 124 L 267.5 118 A 0.50005 0.50005 0 0 0 267.17773 117.5332 C 267.17773 117.5332 266.50667 117.27701 265.66016 117.02148 C 264.81364 116.76596 263.80845 116.5 263 116.5 C 262.19155 116.5 261.47867 116.7832 260.82227 117.0332 C 260.16586 117.2832 259.56592 117.5 259 117.5 C 258.43408 117.5 257.4388 117.26596 256.62891 117.02148 C 256.39123 116.94974 256.17716 116.87994 255.98047 116.81445 A 1 1 0 0 0 255 116 z M 263 117.5 C 263.56592 117.5 264.5612 117.73404 265.37109 117.97852 C 266.00097 118.16865 266.29646 118.28239 266.5 118.35742 L 266.5 120.29297 C 266.25708 120.21012 265.97978 120.11797 265.66016 120.02148 C 264.81364 119.76596 263.80845 119.5 263 119.5 C 262.19155 119.5 261.47867 119.7832 260.82227 120.0332 C 260.16586 120.2832 259.56592 120.5 259 120.5 C 258.43408 120.5 257.4388 120.26596 256.62891 120.02148 C 256.39971 119.9523 256.19148 119.88388 256 119.82031 L 256 117.87109 C 256.1125 117.90694 256.2187 117.94195 256.33984 117.97852 C 257.18636 118.23404 258.19155 118.5 259 118.5 C 259.80845 118.5 260.52133 118.2168 261.17773 117.9668 C 261.83414 117.7168 262.43408 117.5 263 117.5 z M 263 120.5 C 263.56592 120.5 264.5612 120.73404 265.37109 120.97852 C 265.8714 121.12954 266.2398 121.25641 266.5 121.34961 L 266.5 123.30469 C 266.22286 123.20649 266.12863 123.1629 265.66016 123.02148 C 264.81364 122.76596 263.80845 122.5 263 122.5 C 262.19155 122.5 261.47867 122.7832 260.82227 123.0332 C 260.16586 123.2832 259.56592 123.5 259 123.5 C 258.43408 123.5 257.4388 123.26596 256.62891 123.02148 C 256.39971 122.9523 256.19148 122.88388 256 122.82031 L 256 120.87109 C 256.1125 120.90694 256.2187 120.94195 256.33984 120.97852 C 257.18636 121.23404 258.19155 121.5 259 121.5 C 259.80845 121.5 260.52133 121.2168 261.17773 120.9668 C 261.83414 120.7168 262.43408 120.5 263 120.5 z" />
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="DataGridCell.gdp"> <Style Selector="DataGridCell.gdp">
@ -28,7 +34,8 @@
DockPanel.Dock="Top"/> DockPanel.Dock="Top"/>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"> <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" MinWidth="400" /> <!-- Using HeaderTemplate -->
<DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" MinWidth="400" />
<!-- CompiledBinding example of usage. --> <!-- CompiledBinding example of usage. -->
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" /> <DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" /> <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />

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

@ -503,11 +503,24 @@ namespace Avalonia.Collections
{ {
_ = items ?? throw new ArgumentNullException(nameof(items)); _ = items ?? throw new ArgumentNullException(nameof(items));
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. if (hItems.Contains(_inner[i]))
Remove(i); {
counter += 1;
}
else if(counter > 0)
{
RemoveRange(i + 1, counter);
counter = 0;
}
} }
if (counter > 0)
RemoveRange(0, counter);
} }
/// <summary> /// <summary>

27
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -12,6 +12,7 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.MarkupExtensions;
@ -29,6 +30,7 @@ namespace Avalonia.Controls
private bool _settingWidthInternally; private bool _settingWidthInternally;
private int _displayIndexWithFiller; private int _displayIndexWithFiller;
private object _header; private object _header;
private IDataTemplate _headerTemplate;
private DataGridColumnHeader _headerCell; private DataGridColumnHeader _headerCell;
private IControl _editingElement; private IControl _editingElement;
private ICellEditBinding _editBinding; private ICellEditBinding _editBinding;
@ -416,6 +418,28 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets an <see cref="IDataTemplate"/> for the <see cref="Header"/>
/// </summary>
public IDataTemplate HeaderTemplate
{
get
{
return _headerTemplate;
}
set
{
if (_headerTemplate != value)
{
_headerTemplate = value;
if (_headerCell != null)
{
_headerCell.ContentTemplate = value;
}
}
}
}
public bool IsAutoGenerated public bool IsAutoGenerated
{ {
get; get;
@ -850,7 +874,8 @@ namespace Avalonia.Controls
var result = new DataGridColumnHeader var result = new DataGridColumnHeader
{ {
OwningColumn = this, OwningColumn = this,
Content = _header Content = _header,
ContentTemplate = _headerTemplate
}; };
//result.EnsureStyle(null); //result.EnsureStyle(null);

3
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -47,7 +47,8 @@
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"> Margin="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}"/> <ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon" <Path Name="SortIcon"
Grid.Column="1" Grid.Column="1"

3
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -193,7 +193,8 @@
Width="Auto" /> Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}" /> <ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon" <Path Name="SortIcon"
Grid.Column="1" Grid.Column="1"

74
src/Avalonia.Controls/Border.cs

@ -1,3 +1,5 @@
using Avalonia.Collections;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
@ -41,7 +43,31 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty = public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow)); 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(); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary> /// <summary>
@ -54,6 +80,10 @@ namespace Avalonia.Controls
BorderBrushProperty, BorderBrushProperty,
BorderThicknessProperty, BorderThicknessProperty,
CornerRadiusProperty, CornerRadiusProperty,
BorderDashArrayProperty,
BorderLineCapProperty,
BorderLineJoinProperty,
BorderDashOffsetProperty,
BoxShadowProperty); BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty); AffectsMeasure<Border>(BorderThicknessProperty);
} }
@ -76,6 +106,15 @@ namespace Avalonia.Controls
set { SetValue(BorderBrushProperty, value); } 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> /// <summary>
/// Gets or sets the thickness of the border. /// Gets or sets the thickness of the border.
/// </summary> /// </summary>
@ -85,6 +124,33 @@ namespace Avalonia.Controls
set { SetValue(BorderThicknessProperty, value); } 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> /// <summary>
/// Gets or sets the radius of the border rounded corners. /// Gets or sets the radius of the border rounded corners.
/// </summary> /// </summary>
@ -93,7 +159,7 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); } get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); } set { SetValue(CornerRadiusProperty, value); }
} }
/// <summary> /// <summary>
/// Gets or sets the box shadow effect parameters /// Gets or sets the box shadow effect parameters
/// </summary> /// </summary>
@ -102,7 +168,7 @@ namespace Avalonia.Controls
get => GetValue(BoxShadowProperty); get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value); set => SetValue(BoxShadowProperty, value);
} }
/// <summary> /// <summary>
/// Renders the control. /// Renders the control.
/// </summary> /// </summary>
@ -110,7 +176,7 @@ namespace Avalonia.Controls
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow); BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
} }
/// <summary> /// <summary>

55
src/Avalonia.Controls/TextBox.cs

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

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

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

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

@ -357,5 +357,157 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(target, result); 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( private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(), focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(), keyboardDevice: () => new KeyboardDevice(),

Loading…
Cancel
Save