Browse Source

Merge branch 'master' into test-multiple-optimizations-together

pull/18386/head
Dan Walmsley 11 months ago
committed by GitHub
parent
commit
83a15f8c5d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      samples/BindingDemo/BindingDemo.csproj
  2. 13
      samples/BindingDemo/MainWindow.xaml
  3. 2
      samples/BindingDemo/MainWindow.xaml.cs
  4. 4
      samples/BindingDemo/TestItemView.xaml
  5. 22
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  6. 4
      samples/GpuInterop/DrawingSurfaceDemoBase.cs
  7. 16
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  8. 2
      src/Avalonia.Base/Input/InputMethod.cs
  9. 26
      src/Avalonia.Base/Input/KeyGesture.cs
  10. 14
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  11. 48
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs
  12. 6
      src/Avalonia.Base/Media/IGlyphTypeface2.cs
  13. 110
      src/Avalonia.Controls/Grid.cs
  14. 67
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  15. 31
      src/Avalonia.Controls/TextBox.cs
  16. 2
      src/Avalonia.Controls/TopLevel.cs
  17. 15
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  18. 19
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  19. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  20. 35
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  21. 5
      tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs
  22. 70
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  23. 85
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  24. 193
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  25. 174
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  26. 38
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

1
samples/BindingDemo/BindingDemo.csproj

@ -2,6 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<!-- <AvaloniaXamlIlDebuggerLaunch>true</AvaloniaXamlIlDebuggerLaunch>-->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

13
samples/BindingDemo/MainWindow.xaml

@ -3,6 +3,7 @@
x:Class="BindingDemo.MainWindow"
xmlns:vm="using:BindingDemo.ViewModels"
xmlns:local="using:BindingDemo"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
Title="AvaloniaUI Bindings Test"
Width="800"
Height="600"
@ -29,7 +30,7 @@
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/>
<TextBox Watermark="Items[1].Value" UseFloatingWatermark="True" Text="{Binding Path=Items[1].Value}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
@ -51,9 +52,9 @@
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding #first.Text, Mode=TwoWay}"/>
<TextBox Watermark="Value of SharedItem.StringValue" UseFloatingWatermark="True"
Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay, DataType=vm:MainWindowViewModel+TestItem}"/>
Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/>
<TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay, DataType=vm:MainWindowViewModel+TestItem}"/>
Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Scheduler"/>
@ -67,8 +68,8 @@
<TabItem Header="ListBox">
<StackPanel Orientation="Horizontal">
<StackPanel.DataTemplates>
<DataTemplate DataType="vm:MainWindowViewModel+TestItem">
<TextBlock Text="{Binding StringValue}"/>
<DataTemplate x:DataType="{x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}">
<TextBlock Text="{Binding Value}"/>
</DataTemplate>
</StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200">
@ -81,7 +82,7 @@
</StackPanel>
<ContentControl Content="{ReflectionBinding Selection.SelectedItems[0]}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:MainWindowViewModel+TestItem">
<DataTemplate x:DataType="{x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}">
<local:TestItemView></local:TestItemView>
</DataTemplate>
</ContentControl.DataTemplates>

2
samples/BindingDemo/MainWindow.xaml.cs

@ -9,7 +9,7 @@ namespace BindingDemo
{
public MainWindow()
{
Resources["SharedItem"] = new MainWindowViewModel.TestItem() { StringValue = "shared" };
Resources["SharedItem"] = new MainWindowViewModel.TestItem<string>() { Value = "shared" };
this.InitializeComponent();
this.DataContext = new MainWindowViewModel();
this.AttachDevTools();

4
samples/BindingDemo/TestItemView.xaml

@ -2,9 +2,9 @@
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:viewModels="using:BindingDemo.ViewModels"
x:Class="BindingDemo.TestItemView"
x:DataType="viewModels:MainWindowViewModel+TestItem">
x:DataType="{x:Type viewModels:MainWindowViewModel+TestItem, x:TypeArguments=x:String}">
<StackPanel>
<TextBlock Classes="h1" Text="{Binding StringValue}"/>
<TextBlock Classes="h1" Text="{Binding Value}"/>
<TextBox Text="{Binding Detail}" AcceptsReturn="True"/>
</StackPanel>
</UserControl>

22
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -23,14 +23,14 @@ namespace BindingDemo.ViewModels
public MainWindowViewModel()
{
Items = new ObservableCollection<TestItem>(
Enumerable.Range(0, 20).Select(x => new TestItem
Items = new ObservableCollection<TestItem<string>>(
Enumerable.Range(0, 20).Select(x => new TestItem<string>
{
StringValue = "Item " + x,
Value = "Item " + x,
Detail = "Item " + x + " details",
}));
Selection = new SelectionModel<TestItem> { SingleSelect = false };
Selection = new SelectionModel<TestItem<string>> { SingleSelect = false };
ShuffleItems = MiniCommand.Create(() =>
{
@ -58,8 +58,8 @@ namespace BindingDemo.ViewModels
.Select(x => DateTimeOffset.Now);
}
public ObservableCollection<TestItem> Items { get; }
public SelectionModel<TestItem> Selection { get; }
public ObservableCollection<TestItem<string>> Items { get; }
public SelectionModel<TestItem<string>> Selection { get; }
public MiniCommand ShuffleItems { get; }
public string BooleanString
@ -117,15 +117,15 @@ namespace BindingDemo.ViewModels
}
// Nested class, jsut so we can test it in XAML
public class TestItem : ViewModelBase
public class TestItem<T> : ViewModelBase
{
private string _stringValue = "String Value";
private T _value;
private string _detail;
public string StringValue
public T Value
{
get { return _stringValue; }
set { this.RaiseAndSetIfChanged(ref this._stringValue, value); }
get { return _value; }
set { this.RaiseAndSetIfChanged(ref this._value, value); }
}
public string Detail

4
samples/GpuInterop/DrawingSurfaceDemoBase.cs

@ -33,7 +33,11 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_initialized)
{
Surface?.Dispose();
FreeGraphicsResources();
}
_initialized = false;
base.OnDetachedFromLogicalTree(e);
}

16
src/Avalonia.Base/Controls/ResourceDictionary.cs

@ -40,7 +40,8 @@ namespace Avalonia.Controls
set
{
Inner[key] = value;
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
}
@ -150,6 +151,19 @@ namespace Avalonia.Controls
public void AddNotSharedDeferred(object key, IDeferredContent deferredContent)
=> Add(key, new NotSharedDeferredItem(deferredContent));
public void SetItems(IEnumerable<KeyValuePair<object, object?>> values)
{
try
{
foreach (var value in values)
Inner[value.Key] = value.Value;
}
finally
{
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
}
public void Clear()
{
if (_inner?.Count > 0)

2
src/Avalonia.Base/Input/InputMethod.cs

@ -43,7 +43,7 @@ namespace Avalonia.Input
public static void RemoveTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler);
element.RemoveHandler(TextInputMethodClientRequeryRequestedEvent, handler);
}
private InputMethod()

26
src/Avalonia.Base/Input/KeyGesture.cs

@ -79,15 +79,10 @@ namespace Avalonia.Input
{
var partSpan = gesture.AsSpan(cstart, c - cstart).Trim();
if (isLast)
{
key = ParseKey(partSpan.ToString());
}
else
if (!TryParseKey(partSpan.ToString(), out key))
{
keyModifiers |= ParseModifier(partSpan);
}
cstart = c + 1;
}
}
@ -151,8 +146,11 @@ namespace Avalonia.Input
s.Append(formatInfo.Meta);
}
Plus(s);
s.Append(formatInfo.FormatKey(Key));
if ((Key != Key.None) || (KeyModifiers == KeyModifiers.None))
{
Plus(s);
s.Append(formatInfo.FormatKey(Key));
}
return StringBuilderCache.GetStringAndRelease(s);
}
@ -163,12 +161,16 @@ namespace Avalonia.Input
ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key);
// TODO: Move that to external key parser
private static Key ParseKey(string key)
private static bool TryParseKey(string keyStr, out Key key)
{
if (s_keySynonyms.TryGetValue(key.ToLower(CultureInfo.InvariantCulture), out Key rv))
return rv;
key = Key.None;
if (s_keySynonyms.TryGetValue(keyStr.ToLower(CultureInfo.InvariantCulture), out key))
return true;
if (EnumHelper.TryParse(keyStr, true, out key))
return true;
return EnumHelper.Parse<Key>(key, true);
return false;
}
private static KeyModifiers ParseModifier(ReadOnlySpan<char> modifier)

14
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -79,7 +79,9 @@ namespace Avalonia.Input
else if (pointerDevice.TryGetPointer(args) is { } pointer &&
pointer.Type != PointerType.Touch)
{
var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor;
var element = GetEffectivePointerOverElement(
args.InputHitTestResult.firstEnabledAncestor,
pointer.Captured);
SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position,
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
@ -96,7 +98,10 @@ namespace Avalonia.Input
if (dirtyRect.Contains(clientPoint))
{
var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint);
var element = GetEffectivePointerOverElement(
_inputRoot.InputHitTest(clientPoint),
pointer.Captured);
SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint))
@ -106,6 +111,11 @@ namespace Avalonia.Input
}
}
private static IInputElement? GetEffectivePointerOverElement(IInputElement? hitTestElement, IInputElement? captured)
=> captured is not null && hitTestElement != captured ?
null :
hitTestElement;
private void ClearPointerOver()
{
if (_currentPointer is (var pointer, var position))

48
src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs

@ -2,26 +2,25 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts.Tables.Name
{
internal class NameTable
internal class NameTable : IEnumerable<NameRecord>
{
internal const string TableName = "name";
internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
private readonly NameRecord[] _names;
internal NameTable(NameRecord[] names, IReadOnlyList<ushort> languages)
internal NameTable(NameRecord[] names)
{
_names = names;
Languages = languages;
}
public IReadOnlyList<ushort> Languages { get; }
/// <summary>
/// Gets the name of the font.
/// </summary>
@ -133,22 +132,6 @@ namespace Avalonia.Media.Fonts.Tables.Name
}
}
//var languageNames = Array.Empty<StringLoader>();
//if (format == 1)
//{
// // Format 1 adds language data.
// var langCount = reader.ReadUInt16();
// languageNames = new StringLoader[langCount];
// for (var i = 0; i < langCount; i++)
// {
// languageNames[i] = StringLoader.Create(reader);
// strings.Add(languageNames[i]);
// }
//}
foreach (var readable in strings)
{
var readableStartOffset = stringOffset + readable.Offset;
@ -158,22 +141,17 @@ namespace Avalonia.Media.Fonts.Tables.Name
readable.LoadValue(reader);
}
var cultures = new List<ushort>();
foreach (var nameRecord in names)
{
if (nameRecord.NameID != KnownNameIds.FontFamilyName || nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
return new NameTable(names);
}
if (!cultures.Contains(nameRecord.LanguageID))
{
cultures.Add(nameRecord.LanguageID);
}
}
public IEnumerator<NameRecord> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<NameRecord>(_names);
}
return new NameTable(names, cultures);
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

6
src/Avalonia.Base/Media/IGlyphTypeface2.cs

@ -29,5 +29,11 @@ namespace Avalonia.Media
/// Gets supported font features.
/// </summary>
IReadOnlyList<OpenTypeTag> SupportedFeatures { get; }
/// <summary>
/// Gets the localized face names.
/// <para>Keys are culture identifiers.</para>
/// </summary>
IReadOnlyDictionary<ushort, string> FaceNames { get; }
}
}

110
src/Avalonia.Controls/Grid.cs

@ -25,6 +25,8 @@ namespace Avalonia.Controls
static Grid()
{
ShowGridLinesProperty.Changed.AddClassHandler<Grid>(OnShowGridLinesPropertyChanged);
ColumnSpacingProperty.Changed.AddClassHandler<Control>(OnSpacingPropertyChanged);
RowSpacingProperty.Changed.AddClassHandler<Control>(OnSpacingPropertyChanged);
IsSharedSizeScopeProperty.Changed.AddClassHandler<Control>(DefinitionBase.OnIsSharedSizeScopePropertyChanged);
ColumnProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
@ -32,6 +34,7 @@ namespace Avalonia.Controls
RowProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
RowSpanProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
AffectsMeasure<Grid>(ColumnSpacingProperty, RowSpacingProperty);
AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty);
}
@ -161,6 +164,24 @@ namespace Avalonia.Controls
set => SetValue(ShowGridLinesProperty, value);
}
/// <summary>
/// Gets or sets the size of the spacing to place between grid rows.
/// </summary>
public double RowSpacing
{
get => GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
/// <summary>
/// Gets or sets the size of the spacing to place between grid columns.
/// </summary>
public double ColumnSpacing
{
get => GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
/// <summary>
/// Returns a ColumnDefinitions of column definitions.
/// </summary>
@ -299,7 +320,7 @@ namespace Avalonia.Controls
// the cells belonging to them.
//
// However, there are cases when topology of a grid causes cyclical
// size dependences. For example:
// size dependencies. For example:
//
//
// column width="Auto" column width="*"
@ -425,17 +446,19 @@ namespace Avalonia.Controls
//
MeasureCellsGroup(extData.CellGroup1, constraint, false, false);
double combinedRowSpacing = RowSpacing * (RowDefinitions.Count - 1);
double combinedColumnSpacing = ColumnSpacing * (ColumnDefinitions.Count - 1);
Size innerAvailableSize = new Size(constraint.Width - combinedRowSpacing, constraint.Height - combinedColumnSpacing);
{
// after Group1 is measured, only Group3 may have cells belonging to Auto rows.
bool canResolveStarsV = !HasGroup3CellsInAutoRows;
if (canResolveStarsV)
{
if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
MeasureCellsGroup(extData.CellGroup2, constraint, false, false);
if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, false);
if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
}
else
{
@ -444,9 +467,9 @@ namespace Avalonia.Controls
bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length;
if (canResolveStarsU)
{
if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
}
else
{
@ -462,7 +485,7 @@ namespace Avalonia.Controls
double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false);
double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true);
MeasureCellsGroup(extData.CellGroup2, constraint, false, true);
MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, true);
do
{
@ -472,14 +495,14 @@ namespace Avalonia.Controls
ApplyCachedMinSizes(group3MinSizes, true);
}
if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
// Reset cached Group2Widths
ApplyCachedMinSizes(group2MinSizes, false);
if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged);
if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged);
}
while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount);
}
@ -489,8 +512,8 @@ namespace Avalonia.Controls
MeasureCellsGroup(extData.CellGroup4, constraint, false, false);
gridDesiredSize = new Size(
CalculateDesiredSize(DefinitionsU),
CalculateDesiredSize(DefinitionsV));
CalculateDesiredSize(DefinitionsU) + ColumnSpacing * (DefinitionsU.Count - 1),
CalculateDesiredSize(DefinitionsV) + RowSpacing * (DefinitionsU.Count - 1));
}
}
finally
@ -524,9 +547,12 @@ namespace Avalonia.Controls
else
{
Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0);
SetFinalSize(DefinitionsU, arrangeSize.Width, true);
SetFinalSize(DefinitionsV, arrangeSize.Height, false);
double columnSpacing = ColumnSpacing;
double rowSpacing = RowSpacing;
double combinedRowSpacing = rowSpacing * (RowDefinitions.Count - 1);
double combinedColumnSpacing = columnSpacing * (ColumnDefinitions.Count - 1);
SetFinalSize(DefinitionsU, arrangeSize.Width - combinedColumnSpacing, true);
SetFinalSize(DefinitionsV, arrangeSize.Height - combinedRowSpacing, false);
var children = Children;
@ -540,14 +566,13 @@ namespace Avalonia.Controls
int rowSpan = PrivateCells[currentCell].RowSpan;
Rect cellRect = new Rect(
columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset,
rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset,
columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset + (columnSpacing * columnIndex),
rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset + (rowSpacing * rowIndex),
GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan),
GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan));
cell.Arrange(cellRect);
}
// update render bound on grid lines renderer visual
@ -2088,7 +2113,7 @@ namespace Avalonia.Controls
// double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY;
var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
double[] roundingErrors = RoundingErrors;
double roundedTakenSize = 0.0;
double roundedTakenSize = 0;
// round each of the allocated sizes, keeping track of the deltas
for (int i = 0; i < definitions.Count; ++i)
@ -2363,6 +2388,17 @@ namespace Avalonia.Controls
grid.SetFlags((bool)e.NewValue!, Flags.ShowGridLinesPropertyValue);
}
private static void OnSpacingPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
Grid grid = (Grid)d;
if (grid._extData != null
&& grid.ListenToNotifications)
{
grid.CellsStructureDirty = true;
}
}
private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
if (d is Visual child)
@ -2674,6 +2710,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> ShowGridLinesProperty =
AvaloniaProperty.Register<Grid, bool>(nameof(ShowGridLines));
/// <summary>
/// Defines the <see cref="RowSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> RowSpacingProperty =
AvaloniaProperty.Register<Grid, double>(nameof(RowSpacing));
/// <summary>
/// Defines the <see cref="ColumnSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> ColumnSpacingProperty =
AvaloniaProperty.Register<Grid, double>(nameof(ColumnSpacingProperty));
/// <summary>
/// Column property. This is an attached property.
/// Grid defines Column property, so that it can be set
@ -3269,6 +3317,14 @@ namespace Avalonia.Controls
drawingContext,
grid.ColumnDefinitions[i].FinalOffset, 0.0,
grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height);
if (grid.ColumnSpacing != 0)
{
DrawGridLine(
drawingContext,
grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, 0.0,
grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, _lastArrangeSize.Height);
}
}
for (int i = 1; i < grid.RowDefinitions.Count; ++i)
@ -3277,6 +3333,14 @@ namespace Avalonia.Controls
drawingContext,
0.0, grid.RowDefinitions[i].FinalOffset,
_lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset);
if (grid.RowSpacing != 0)
{
DrawGridLine(
drawingContext,
0.0, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing,
_lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing);
}
}
}

67
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -257,28 +257,12 @@ namespace Avalonia.Controls.Presenters
return false;
}
var rect = targetRect.TransformToAABB(transform.Value);
var offset = Offset;
var rectangle = targetRect.TransformToAABB(transform.Value).Deflate(new Thickness(Child.Margin.Left, Child.Margin.Top, 0, 0));
Rect viewport = new Rect(Offset.X, Offset.Y, Viewport.Width, Viewport.Height);
if (rect.Bottom > offset.Y + Viewport.Height)
{
offset = offset.WithY((rect.Bottom - Viewport.Height) + Child.Margin.Top);
}
if (rect.Y < offset.Y)
{
offset = offset.WithY(rect.Y);
}
if (rect.Right > offset.X + Viewport.Width)
{
offset = offset.WithX((rect.Right - Viewport.Width) + Child.Margin.Left);
}
if (rect.X < offset.X)
{
offset = offset.WithX(rect.X);
}
double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right);
double minY = ComputeScrollOffsetWithMinimalScroll(viewport.Top, viewport.Bottom, rectangle.Top, rectangle.Bottom);
var offset = new Vector(minX, minY);
if (Offset.NearlyEquals(offset))
{
@ -293,6 +277,47 @@ namespace Avalonia.Controls.Presenters
return !Offset.NearlyEquals(oldOffset);
}
/// <summary>
/// Computes the closest offset to ensure most of the child is visible in the viewport along an axis.
/// </summary>
/// <param name="viewportStart">The left or top of the viewport</param>
/// <param name="viewportEnd">The right or bottom of the viewport</param>
/// <param name="childStart">The left or top of the child</param>
/// <param name="childEnd">The right or bottom of the child</param>
/// <returns></returns>
internal static double ComputeScrollOffsetWithMinimalScroll(
double viewportStart,
double viewportEnd,
double childStart,
double childEnd)
{
// If child is at least partially above viewport, i.e. top of child is above viewport top and bottom of child is above viewport bottom.
bool isChildAbove = MathUtilities.LessThan(childStart, viewportStart) && MathUtilities.LessThan(childEnd, viewportEnd);
// If child is at least partially below viewport, i.e. top of child is below viewport top and bottom of child is below viewport bottom.
bool isChildBelow = MathUtilities.GreaterThan(childEnd, viewportEnd) && MathUtilities.GreaterThan(childStart, viewportStart);
bool isChildLarger = (childEnd - childStart) > (viewportEnd - viewportStart);
// Value if no updates is needed. The child is fully visible in the viewport, or the viewport is completely within the child's bounds
var res = viewportStart;
// The child is above the viewport and is smaller than the viewport, or if the child's top is below the viewport top
// and is larger than the viewport, we align the child top to the top of the viewport
if ((isChildAbove && !isChildLarger)
|| (isChildBelow && isChildLarger))
{
res = childStart;
}
// The child is above the viewport and is larger than the viewport, or if the child's smaller but is below the viewport,
// we align the child's bottom to the bottom of the viewport
else if (isChildAbove || isChildBelow)
{
res = (childEnd - (viewportEnd - viewportStart));
}
return res;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

31
src/Avalonia.Controls/TextBox.cs

@ -1467,6 +1467,11 @@ namespace Avalonia.Controls
{
selection = DetectSelection();
if (!selection && SelectionStart != SelectionEnd)
{
ClearSelectionAndMoveCaretToTextPosition(LogicalDirection.Backward);
}
_presenter.MoveCaretVertical(LogicalDirection.Backward);
if (caretIndex != _presenter.CaretIndex)
@ -1489,6 +1494,11 @@ namespace Avalonia.Controls
{
selection = DetectSelection();
if (!selection && SelectionStart != SelectionEnd)
{
ClearSelectionAndMoveCaretToTextPosition(LogicalDirection.Forward);
}
_presenter.MoveCaretVertical();
if (caretIndex != _presenter.CaretIndex)
@ -1983,13 +1993,9 @@ namespace Avalonia.Controls
{
if (selectionStart != selectionEnd)
{
// clear the selection and move to the appropriate side of previous selection
var newPosition = direction > 0 ?
Math.Max(selectionStart, selectionEnd) :
Math.Min(selectionStart, selectionEnd);
SetCurrentValue(SelectionStartProperty, newPosition);
SetCurrentValue(SelectionEndProperty, newPosition);
_presenter.MoveCaretToTextPosition(newPosition);
ClearSelectionAndMoveCaretToTextPosition(direction > 0 ?
LogicalDirection.Forward :
LogicalDirection.Backward);
}
else
{
@ -2100,6 +2106,17 @@ namespace Avalonia.Controls
_scrollViewer?.PageDown();
}
private void ClearSelectionAndMoveCaretToTextPosition(LogicalDirection direction)
{
var newPosition = direction == LogicalDirection.Forward ?
Math.Max(SelectionStart, SelectionEnd) :
Math.Min(SelectionStart, SelectionEnd);
SetCurrentValue(SelectionStartProperty, newPosition);
SetCurrentValue(SelectionEndProperty, newPosition);
// move caret to appropriate side of previous selection
_presenter?.MoveCaretToTextPosition(newPosition);
}
/// <summary>
/// Scroll the <see cref="TextBox"/> to the specified line index.
/// </summary>

2
src/Avalonia.Controls/TopLevel.cs

@ -869,7 +869,7 @@ namespace Avalonia.Controls
var candidate = hitTestElement;
while (candidate?.IsEffectivelyEnabled == false)
{
candidate = (candidate as Visual)?.Parent as IInputElement;
candidate = (candidate as Visual)?.VisualParent as IInputElement;
}
return candidate;

15
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -13,16 +13,23 @@ internal static partial class InputHelper
return Task.CompletedTask;
}
public static Task<T> RedirectInputRetunAsync<T>(int topLevelId, Func<BrowserTopLevelImpl,T> handler, T @default)
{
if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl)
return Task.FromResult(handler(topLevelImpl));
return Task.FromResult(@default);
}
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeInputEvents(JSObject htmlElement, int topLevelId);
[JSExport]
public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) =>
RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier));
public static Task<bool> OnKeyDown(int topLevelId, string code, string key, int modifier) =>
RedirectInputRetunAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier), false);
[JSExport]
public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) =>
RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier));
public static Task<bool> OnKeyUp(int topLevelId, string code, string key, int modifier) =>
RedirectInputRetunAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier), false);
[JSExport]
public static Task OnBeforeInput(int topLevelId, string inputType, int start, int end) =>

19
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -95,16 +95,23 @@ export class InputHelper {
public static subscribeKeyEvents(element: HTMLInputElement, topLevelId: number) {
const keyDownHandler = (args: KeyboardEvent) => {
JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args));
if (this.clipboardState !== ClipboardState.Pending) {
args.preventDefault();
}
JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args))
.then((handled: boolean) => {
if (!handled || this.clipboardState !== ClipboardState.Pending) {
args.preventDefault();
}
});
};
element.addEventListener("keydown", keyDownHandler);
const keyUpHandler = (args: KeyboardEvent) => {
JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args));
args.preventDefault();
JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args))
.then((handled: boolean) => {
if (!handled) {
args.preventDefault();
}
});
if (this.rejectClipboard) {
this.rejectClipboard();
}

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -44,7 +44,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
on.Children.RemoveAt(i);
i--;
if (directive.Values[0] is XamlAstTextNode text)
if (directive.Values[0] is XamlTypeExtensionNode typeNode)
{
directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, typeNode.Value.GetClrType());
}
else if (directive.Values[0] is XamlAstTextNode text)
{
directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on,
TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type);

35
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -111,18 +111,45 @@ namespace Avalonia.Skia
if(_nameTable != null)
{
var familyNames = new Dictionary<ushort, string>(_nameTable.Languages.Count);
var familyNames = new Dictionary<ushort, string>(1);
var faceNames = new Dictionary<ushort, string>(1);
foreach (var language in _nameTable.Languages)
foreach (var nameRecord in _nameTable)
{
familyNames.Add(language, _nameTable.FontFamilyName(language));
if(nameRecord.NameID == KnownNameIds.FontFamilyName)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
if (!familyNames.ContainsKey(nameRecord.LanguageID))
{
familyNames[nameRecord.LanguageID] = nameRecord.Value;
}
}
if(nameRecord.NameID == KnownNameIds.FontSubfamilyName)
{
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
{
continue;
}
if (!faceNames.ContainsKey(nameRecord.LanguageID))
{
faceNames[nameRecord.LanguageID] = nameRecord.Value;
}
}
}
FamilyNames = familyNames;
FaceNames = faceNames;
}
else
{
FamilyNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, FamilyName } };
FaceNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, Weight.ToString() } };
}
}
@ -130,6 +157,8 @@ namespace Avalonia.Skia
public IReadOnlyDictionary<ushort, string> FamilyNames { get; }
public IReadOnlyDictionary<ushort, string> FaceNames { get; }
public IReadOnlyList<OpenTypeTag> SupportedFeatures
{
get

5
tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs

@ -13,6 +13,9 @@ namespace Avalonia.Base.UnitTests.Input
new object[]{"Control++", new KeyGesture(Key.OemPlus, KeyModifiers.Control) },
new object[]{ "Shift+⌘+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
new object[]{ "Shift+Cmd+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
new object[]{"None", new KeyGesture(Key.None)},
new object[]{"Alt+Shift", new KeyGesture(Key.None, KeyModifiers.Alt | KeyModifiers.Shift)},
};
public static readonly IEnumerable<object[]> ToStringData = new object[][]
@ -23,6 +26,8 @@ namespace Avalonia.Base.UnitTests.Input
new object[]{new KeyGesture(Key.A, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift), "Ctrl+Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift), "Shift+Cmd+A"},
new object[]{new KeyGesture(Key.None), "None"},
new object[]{new KeyGesture(Key.None, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt"},
};
[Theory]

70
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -6,6 +6,7 @@ using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
@ -119,7 +120,7 @@ namespace Avalonia.Base.UnitTests.Input
}
[Fact]
public void HitTest_Should_Be_Ignored_If_Element_Captured()
public void HitTest_Should_Ignore_Non_Captured_Elements()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
@ -144,8 +145,19 @@ namespace Avalonia.Base.UnitTests.Input
}
}, renderer.Object);
SetHit(renderer, canvas);
pointer.SetupGet(p => p.Captured).Returns(decorator);
// Move the pointer over the canvas: the captured decorator should lose the pointer over state.
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.False(root.IsPointerOver);
// Move back the pointer over the decorator: raise events normally for it since it's captured.
SetHit(renderer, decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.True(decorator.IsPointerOver);
@ -494,6 +506,60 @@ namespace Avalonia.Base.UnitTests.Input
result);
}
[Fact]
public void Disabled_Element_Should_Set_PointerOver_On_Visual_Parent()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IHitTester>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock();
var disabledChild = new Border
{
Background = Brushes.Red,
Width = 100,
Height = 100,
IsEnabled = false
};
var visualParent = new Border
{
Background = Brushes.Black,
Width = 100,
Height = 100,
Child = disabledChild
};
var logicalParent = new Border
{
Background = Brushes.Blue,
Width = 100,
Height = 100
};
// Change the logical parent and check that we're correctly hit testing on the visual tree.
// This scenario is made up because it's easy to test.
// In the real world, this happens with nested Popups from MenuItems (but that's very cumbersome to test).
((ISetLogicalParent) disabledChild).SetParent(null);
((ISetLogicalParent) disabledChild).SetParent(logicalParent);
var root = CreateInputRoot(
impl.Object,
new Panel
{
Children = { visualParent }
},
renderer.Object);
Assert.False(visualParent.IsPointerOver);
SetHit(renderer, disabledChild);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, new Point(50, 50)));
Assert.True(visualParent.IsPointerOver);
Assert.False(logicalParent.IsPointerOver);
}
private static void AddEnteredExitedHandlers(
EventHandler<PointerEventArgs> handler,
params IInputElement[] controls)

85
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1656,6 +1656,91 @@ namespace Avalonia.Controls.UnitTests
Assert.False(grid.IsArrangeValid);
}
[Fact]
public void Should_Grid_Controls_With_Spacing()
{
var target = new Grid
{
RowSpacing = 10,
ColumnSpacing = 10,
RowDefinitions = RowDefinitions.Parse("100,100"),
ColumnDefinitions = ColumnDefinitions.Parse("100,100"),
Children =
{
new Border(),
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.RowProperty] = 1 },
new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Rect(0, 0, 210, 210), target.Bounds);
Assert.Equal(new Rect(0, 0, 100, 100), target.Children[0].Bounds);
Assert.Equal(new Rect(110, 0, 100, 100), target.Children[1].Bounds);
Assert.Equal(new Rect(0, 110, 100, 100), target.Children[2].Bounds);
Assert.Equal(new Rect(110, 110, 100, 100), target.Children[3].Bounds);
}
[Fact]
public void Should_Grid_Controls_With_Spacing_Complicated()
{
var target = new Grid
{
Width = 200,
Height = 200,
RowSpacing = 10,
ColumnSpacing = 10,
RowDefinitions = RowDefinitions.Parse("50,*,2*,Auto"),
ColumnDefinitions = ColumnDefinitions.Parse("50,*,2*,Auto"),
Children =
{
new Border(),
new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 },
new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 },
new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 },
},
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Rect(0, 0, 200, 200), target.Bounds);
Assert.Equal(new Rect(0, 0, 50, 50), target.Children[0].Bounds);
Assert.Equal(new Rect(60, 60, 30, 30), target.Children[1].Bounds);
Assert.Equal(new Rect(100, 100, 60, 60), target.Children[2].Bounds);
Assert.Equal(new Rect(170, 170, 30, 30), target.Children[3].Bounds);
}
[Fact]
public void Should_Grid_Controls_With_Spacing_Overflow()
{
var target = new Grid
{
Width = 100,
Height = 100,
ColumnSpacing = 20,
RowSpacing = 20,
ColumnDefinitions = ColumnDefinitions.Parse("30,*,*,Auto"),
RowDefinitions = RowDefinitions.Parse("30,*,*,Auto"),
Children =
{
new Border(),
new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 },
new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 },
new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 },
},
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Rect(0, 0, 100, 100), target.Bounds);
Assert.Equal(new Rect(0, 0, 30, 30), target.Children[0].Bounds);
Assert.Equal(new Rect(50, 50, 0, 0), target.Children[1].Bounds);
Assert.Equal(new Rect(70, 70, 0, 0), target.Children[2].Bounds);
Assert.Equal(new Rect(90, 90, 30, 30), target.Children[3].Bounds);
}
private class TestControl : Control
{
public Size MeasureSize { get; set; }

193
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
using Xunit.Sdk;
namespace Avalonia.Controls.UnitTests.Presenters
{
@ -402,6 +403,198 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Vector(150, 150), target.Offset);
}
[Fact]
public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_In_View()
{
Border border = new Border
{
Width = 100,
Height = 20
};
var content = new StackPanel()
{
Orientation = Orientation.Vertical,
Width = 100,
};
for(int i = 0; i < 100; i++)
{
// border position will be (0,60)
var child = i == 3 ? border : new Border
{
Width = 100,
Height = 20,
};
content.Children.Add(child);
}
var target = new ScrollContentPresenter
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Width = 200,
Height = 100,
Content = new Decorator
{
Child = content
}
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
Assert.Equal(new Vector(0, 0), target.Offset);
}
[Fact]
public void BringDescendantIntoView_Should_Move_Child_At_Least_Partially_Above_Viewport()
{
Border border = new Border
{
Width = 100,
Height = 20
};
var content = new StackPanel()
{
Orientation = Orientation.Vertical,
Width = 100,
};
for(int i = 0; i < 100; i++)
{
// border position will be (0,60)
var child = i == 3 ? border : new Border
{
Width = 100,
Height = 20,
};
content.Children.Add(child);
}
var target = new ScrollContentPresenter
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Width = 200,
Height = 100,
Content = new Decorator
{
Child = content
}
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
// move border to above the view port
target.Offset = new Vector(0, 90);
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
Assert.Equal(new Vector(0, 60), target.Offset);
// move border to partially above the view port
target.Offset = new Vector(0, 70);
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
Assert.Equal(new Vector(0, 60), target.Offset);
}
[Fact]
public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_Covers_Viewport()
{
Border border = new Border
{
Width = 100,
Height = 200
};
var content = new StackPanel()
{
Orientation = Orientation.Vertical,
Width = 100,
};
for (int i = 0; i < 100; i++)
{
// border position will be (0,60)
var child = i == 3 ? border : new Border
{
Width = 100,
Height = 20,
};
content.Children.Add(child);
}
var target = new ScrollContentPresenter
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Width = 200,
Height = 100,
Content = new Decorator
{
Child = content
}
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
// move border such that it's partially above viewport and partially below viewport
target.Offset = new Vector(0, 90);
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
Assert.Equal(new Vector(0, 90), target.Offset);
}
[Fact]
public void BringDescendantIntoView_Should_Move_Child_At_Least_Partially_Below_Viewport()
{
Border border = new Border
{
Width = 100,
Height = 20
};
var content = new StackPanel()
{
Orientation = Orientation.Vertical,
Width = 100,
};
for (int i = 0; i < 100; i++)
{
// border position will be (0,180)
var child = i == 9 ? border : new Border
{
Width = 100,
Height = 20,
};
content.Children.Add(child);
}
var target = new ScrollContentPresenter
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Width = 200,
Height = 100,
Content = new Decorator
{
Child = content
}
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
// border is at (0, 180) and below the viewport
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
Assert.Equal(new Vector(0, 100), target.Offset);
// move border to partially below the view port
target.Offset = new Vector(0, 90);
target.BringDescendantIntoView(border, new Rect(border.Bounds.Size));
}
[Fact]
public void Nested_Presenters_Should_Scroll_Outer_When_Content_Exceeds_Viewport()
{

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

@ -1540,6 +1540,7 @@ namespace Avalonia.Controls.UnitTests
[InlineData(0,4)]
[InlineData(2,6)]
[InlineData(0,6)]
[InlineData(3,4)]
public void When_Selection_From_Left_To_Right_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@ -1568,6 +1569,7 @@ namespace Avalonia.Controls.UnitTests
[InlineData(0,4)]
[InlineData(2,6)]
[InlineData(0,6)]
[InlineData(3,4)]
public void When_Selection_From_Left_To_Right_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@ -1596,6 +1598,7 @@ namespace Avalonia.Controls.UnitTests
[InlineData(4,0)]
[InlineData(6,2)]
[InlineData(6,0)]
[InlineData(4,3)]
public void When_Selection_From_Right_To_Left_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@ -1624,6 +1627,7 @@ namespace Avalonia.Controls.UnitTests
[InlineData(4,0)]
[InlineData(6,2)]
[InlineData(6,0)]
[InlineData(4,3)]
public void When_Selection_From_Right_To_Left_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@ -1701,6 +1705,176 @@ namespace Avalonia.Controls.UnitTests
}
}
[Theory]
[InlineData(2,4)]
[InlineData(0,4)]
[InlineData(2,6)]
[InlineData(0,6)]
[InlineData(3,4)]
public void When_Selection_From_Left_To_Right_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = selectionStart;
tb.SelectionStart = selectionStart;
tb.SelectionEnd = selectionEnd;
RaiseKeyEvent(tb, Key.Up, KeyModifiers.None);
Assert.Equal(selectionStart, tb.SelectionStart);
Assert.Equal(selectionStart, tb.SelectionEnd);
Assert.Equal(selectionStart, tb.CaretIndex);
}
}
[Theory]
[InlineData(4,2)]
[InlineData(4,0)]
[InlineData(6,2)]
[InlineData(6,0)]
[InlineData(4,3)]
public void When_Selection_From_Right_To_Left_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = selectionStart;
tb.SelectionStart = selectionStart;
tb.SelectionEnd = selectionEnd;
RaiseKeyEvent(tb, Key.Up, KeyModifiers.None);
Assert.Equal(selectionEnd, tb.SelectionStart);
Assert.Equal(selectionEnd, tb.SelectionEnd);
Assert.Equal(selectionEnd, tb.CaretIndex);
}
}
[Theory]
[InlineData(0)]
[InlineData(2)]
[InlineData(4)]
[InlineData(6)]
public void When_Select_All_From_Position_Up_Should_Remove_Selection_Moving_Caret_To_Start(int caretIndex)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = caretIndex;
RaiseKeyEvent(tb, Key.A, KeyModifiers.Control);
RaiseKeyEvent(tb, Key.Up, KeyModifiers.None);
Assert.Equal(0, tb.SelectionStart);
Assert.Equal(0, tb.SelectionEnd);
Assert.Equal(0, tb.CaretIndex);
}
}
[Theory]
[InlineData(2,4)]
[InlineData(0,4)]
[InlineData(2,6)]
[InlineData(0,6)]
[InlineData(3,4)]
public void When_Selection_From_Left_To_Right_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = selectionStart;
tb.SelectionStart = selectionStart;
tb.SelectionEnd = selectionEnd;
RaiseKeyEvent(tb, Key.Down, KeyModifiers.None);
Assert.Equal(selectionEnd, tb.SelectionStart);
Assert.Equal(selectionEnd, tb.SelectionEnd);
Assert.Equal(selectionEnd, tb.CaretIndex);
}
}
[Theory]
[InlineData(4,2)]
[InlineData(4,0)]
[InlineData(6,2)]
[InlineData(6,0)]
[InlineData(4,3)]
public void When_Selection_From_Right_To_Left_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = selectionStart;
tb.SelectionStart = selectionStart;
tb.SelectionEnd = selectionEnd;
RaiseKeyEvent(tb, Key.Down, KeyModifiers.None);
Assert.Equal(selectionStart, tb.SelectionStart);
Assert.Equal(selectionStart, tb.SelectionEnd);
Assert.Equal(selectionStart, tb.CaretIndex);
}
}
[Theory]
[InlineData(0)]
[InlineData(2)]
[InlineData(4)]
[InlineData(6)]
public void When_Select_All_From_Position_Down_Should_Remove_Selection_Moving_Caret_To_End(int caretIndex)
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "ABCDEF"
};
tb.Measure(Size.Infinity);
tb.CaretIndex = caretIndex;
RaiseKeyEvent(tb, Key.A, KeyModifiers.Control);
RaiseKeyEvent(tb, Key.Down, KeyModifiers.None);
Assert.Equal(tb.Text.Length, tb.SelectionStart);
Assert.Equal(tb.Text.Length, tb.SelectionEnd);
Assert.Equal(tb.Text.Length, tb.CaretIndex);
}
}
[Fact]
public void TextBox_In_AdornerLayer_Will_Not_Cause_Collection_Modified_In_VisualLayerManager_Measure()
{

38
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -2369,6 +2369,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void Resolves_Nested_Generic_DataTypes()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='{x:Type local:TestDataContext+NestedGeneric, x:TypeArguments=x:String}'
x:Name='MyWindow'>
<Panel>
<TextBlock Text='{CompiledBinding Value}' Name='textBlock' />
</Panel>
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
NestedGenericString = new TestDataContext.NestedGeneric<string>
{
Value = "10"
}
};
window.DataContext = dataContext.NestedGenericString;
Assert.Equal(dataContext.NestedGenericString.Value, textBlock.Text);
}
}
static void Throws(string type, Action cb)
{
try
@ -2459,6 +2490,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty;
public NestedGeneric<string>? NestedGenericString { get; init; }
string IHasExplicitProperty.ExplicitProperty => "Hello";
public string ExplicitProperty => "Bye";
@ -2484,6 +2517,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
}
public class NestedGeneric<T>
{
public T Value { get; set; }
}
}
public class ListItemCollectionView<T> : List<T>

Loading…
Cancel
Save