diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b70e0bf77f..54645e461e 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -68,7 +68,7 @@ jobs:
inputs:
script: |
brew update
- brew install castxml
+ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2
displayName: 'Install Nuke'
diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs
index ceda1c397f..a2b4b86bac 100644
--- a/src/Avalonia.Base/Threading/DispatcherPriority.cs
+++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Threading
SystemIdle = 1,
///
- /// The job will be processed when the application sis idle.
+ /// The job will be processed when the application is idle.
///
ApplicationIdle = 2,
diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs
index 84c3ededa5..3fd84c0c7b 100644
--- a/src/Avalonia.Controls/Shapes/Path.cs
+++ b/src/Avalonia.Controls/Shapes/Path.cs
@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Shapes
@@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
static Path()
{
AffectsGeometry(DataProperty);
+ DataProperty.Changed.AddClassHandler((o, e) => o.DataChanged(e));
}
public Geometry Data
@@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
}
protected override Geometry CreateDefiningGeometry() => Data;
+
+ private void DataChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var oldGeometry = (Geometry)e.OldValue;
+ var newGeometry = (Geometry)e.NewValue;
+
+ if (oldGeometry is object)
+ {
+ oldGeometry.Changed -= GeometryChanged;
+ }
+
+ if (newGeometry is object)
+ {
+ newGeometry.Changed += GeometryChanged;
+ }
+ }
+
+ private void GeometryChanged(object sender, EventArgs e)
+ {
+ InvalidateGeometry();
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 680d7e7d2b..06624c555f 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -473,8 +473,10 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
+ _undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
+ _undoRedoHelper.Snapshot();
}
handled = true;
@@ -600,6 +602,7 @@ namespace Avalonia.Controls
break;
case Key.Back:
+ _undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
@@ -623,11 +626,13 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
}
+ _undoRedoHelper.Snapshot();
handled = true;
break;
case Key.Delete:
+ _undoRedoHelper.Snapshot();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@@ -649,6 +654,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
+ _undoRedoHelper.Snapshot();
handled = true;
break;
@@ -656,7 +662,9 @@ namespace Avalonia.Controls
case Key.Enter:
if (AcceptsReturn)
{
+ _undoRedoHelper.Snapshot();
HandleTextInput(NewLine);
+ _undoRedoHelper.Snapshot();
handled = true;
}
@@ -665,7 +673,9 @@ namespace Avalonia.Controls
case Key.Tab:
if (AcceptsTab)
{
+ _undoRedoHelper.Snapshot();
HandleTextInput("\t");
+ _undoRedoHelper.Snapshot();
handled = true;
}
else
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index a224ceaadd..d3bd45d13c 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -51,6 +51,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue(true);
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
+ ParentProperty.Changed.AddClassHandler((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e));
}
@@ -179,5 +180,16 @@ namespace Avalonia.Controls
return logical != null ? result : @default;
}
+
+ private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)
+ {
+ // If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be
+ // called when the item is removed. This results in the item not being removed from the index,
+ // causing #3551. In this case, update the index when Parent is changed to null.
+ ItemContainerGenerator.UpdateIndex();
+ }
+ }
}
}
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index bcae8a3c53..011ae6ce6b 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -53,11 +53,11 @@ namespace Avalonia.Input
///
/// The control to focus.
/// The method by which focus was changed.
- /// Any input modifiers active at the time of focus.
+ /// Any key modifiers active at the time of focus.
public void Focus(
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
- InputModifiers modifiers = InputModifiers.None)
+ KeyModifiers keyModifiers = KeyModifiers.None)
{
if (control != null)
{
@@ -67,7 +67,7 @@ namespace Avalonia.Input
if (scope != null)
{
Scope = scope;
- SetFocusedElement(scope, control, method, modifiers);
+ SetFocusedElement(scope, control, method, keyModifiers);
}
}
else if (Current != null)
@@ -95,7 +95,7 @@ namespace Avalonia.Input
/// The focus scope.
/// The element to focus. May be null.
/// The method by which focus was changed.
- /// Any input modifiers active at the time of focus.
+ /// Any key modifiers active at the time of focus.
///
/// If the specified scope is the current then the keyboard focus
/// will change.
@@ -104,7 +104,7 @@ namespace Avalonia.Input
IFocusScope scope,
IInputElement element,
NavigationMethod method = NavigationMethod.Unspecified,
- InputModifiers modifiers = InputModifiers.None)
+ KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires(scope != null);
@@ -123,7 +123,7 @@ namespace Avalonia.Input
if (Scope == scope)
{
- KeyboardDevice.Instance?.SetFocusedElement(element, method, modifiers);
+ KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers);
}
}
@@ -195,7 +195,7 @@ namespace Avalonia.Input
{
if (element is IInputElement inputElement && CanFocus(inputElement))
{
- Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
+ Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers);
break;
}
diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs
index 84cd791ee0..9122cc428d 100644
--- a/src/Avalonia.Input/IFocusManager.cs
+++ b/src/Avalonia.Input/IFocusManager.cs
@@ -20,11 +20,11 @@ namespace Avalonia.Input
///
/// The control to focus.
/// The method by which focus was changed.
- /// Any input modifiers active at the time of focus.
+ /// Any key modifiers active at the time of focus.
void Focus(
- IInputElement control,
+ IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
- InputModifiers modifiers = InputModifiers.None);
+ KeyModifiers keyModifiers = KeyModifiers.None);
///
/// Notifies the focus manager of a change in focus scope.
diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs
index 2725638b9a..ba7e0484ee 100644
--- a/src/Avalonia.Input/IKeyboardDevice.cs
+++ b/src/Avalonia.Input/IKeyboardDevice.cs
@@ -63,6 +63,6 @@ namespace Avalonia.Input
void SetFocusedElement(
IInputElement element,
NavigationMethod method,
- InputModifiers modifiers);
+ KeyModifiers modifiers);
}
}
diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs
index 4d0ae7e85d..88d00b3b50 100644
--- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs
@@ -19,10 +19,10 @@ namespace Avalonia.Input
///
/// The current element.
/// The direction to move.
- /// Any input modifiers active at the time of focus.
+ /// Any key modifiers active at the time of focus.
void Move(
IInputElement element,
NavigationDirection direction,
- InputModifiers modifiers = InputModifiers.None);
+ KeyModifiers keyModifiers = KeyModifiers.None);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs
index 006a6b12d9..0321b0bdf3 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -35,7 +35,7 @@ namespace Avalonia.Input
public void SetFocusedElement(
IInputElement element,
NavigationMethod method,
- InputModifiers modifiers)
+ KeyModifiers keyModifiers)
{
if (element != FocusedElement)
{
@@ -53,7 +53,7 @@ namespace Avalonia.Input
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method,
- InputModifiers = modifiers,
+ KeyModifiers = keyModifiers,
});
}
}
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index 323a225b50..c425eeeedb 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs
@@ -91,11 +91,11 @@ namespace Avalonia.Input
///
/// The current element.
/// The direction to move.
- /// Any input modifiers active at the time of focus.
+ /// Any key modifiers active at the time of focus.
public void Move(
IInputElement element,
NavigationDirection direction,
- InputModifiers modifiers = InputModifiers.None)
+ KeyModifiers keyModifiers = KeyModifiers.None)
{
Contract.Requires(element != null);
@@ -106,7 +106,7 @@ namespace Avalonia.Input
var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional;
- FocusManager.Instance.Focus(next, method, modifiers);
+ FocusManager.Instance.Focus(next, method, keyModifiers);
}
}
@@ -123,7 +123,7 @@ namespace Avalonia.Input
{
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
- Move(current, direction, e.Modifiers);
+ Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
}
diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
index bbd5515da0..62a1dd5d84 100644
--- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
///
/// Gets the type of the event.
///
- public RawPointerEventType Type { get; private set; }
+ public RawPointerEventType Type { get; set; }
///
/// Gets the input modifiers.
diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs
index 5622731a7c..d22566442a 100644
--- a/src/Avalonia.Layout/AttachedLayout.cs
+++ b/src/Avalonia.Layout/AttachedLayout.cs
@@ -46,7 +46,23 @@ namespace Avalonia.Layout
/// to provide the behavior for
/// this method in a derived class.
///
- public abstract void InitializeForContext(LayoutContext context);
+ public void InitializeForContext(LayoutContext context)
+ {
+ if (this is VirtualizingLayout virtualizingLayout)
+ {
+ var virtualizingContext = GetVirtualizingLayoutContext(context);
+ virtualizingLayout.InitializeForContextCore(virtualizingContext);
+ }
+ else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+ {
+ var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+ nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
///
/// Removes any state the layout previously stored on the ILayoutable container.
@@ -55,7 +71,23 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- public abstract void UninitializeForContext(LayoutContext context);
+ public void UninitializeForContext(LayoutContext context)
+ {
+ if (this is VirtualizingLayout virtualizingLayout)
+ {
+ var virtualizingContext = GetVirtualizingLayoutContext(context);
+ virtualizingLayout.UninitializeForContextCore(virtualizingContext);
+ }
+ else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+ {
+ var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+ nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
///
/// Suggests a DesiredSize for a container element. A container element that supports
@@ -73,7 +105,23 @@ namespace Avalonia.Layout
/// if scrolling or other resize behavior is possible in that particular container.
///
///
- public abstract Size Measure(LayoutContext context, Size availableSize);
+ public Size Measure(LayoutContext context, Size availableSize)
+ {
+ if (this is VirtualizingLayout virtualizingLayout)
+ {
+ var virtualizingContext = GetVirtualizingLayoutContext(context);
+ return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
+ }
+ else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+ {
+ var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+ return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
///
/// Positions child elements and determines a size for a container UIElement. Container
@@ -88,7 +136,23 @@ namespace Avalonia.Layout
/// The final size that the container computes for the child in layout.
///
/// The actual size that is used after the element is arranged in layout.
- public abstract Size Arrange(LayoutContext context, Size finalSize);
+ public Size Arrange(LayoutContext context, Size finalSize)
+ {
+ if (this is VirtualizingLayout virtualizingLayout)
+ {
+ var virtualizingContext = GetVirtualizingLayoutContext(context);
+ return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
+ }
+ else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+ {
+ var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+ return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
///
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously.
///
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
+
+ private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
+ {
+ if (context is VirtualizingLayoutContext virtualizingContext)
+ {
+ return virtualizingContext;
+ }
+ else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
+ {
+ return nonVirtualizingContext.GetVirtualizingContextAdapter();
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
+ {
+ if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
+ {
+ return nonVirtualizingContext;
+ }
+ else if (context is VirtualizingLayoutContext virtualizingContext)
+ {
+ return virtualizingContext.GetNonVirtualizingContextAdapter();
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
}
}
diff --git a/src/Avalonia.Layout/LayoutContextAdapter.cs b/src/Avalonia.Layout/LayoutContextAdapter.cs
new file mode 100644
index 0000000000..695866df94
--- /dev/null
+++ b/src/Avalonia.Layout/LayoutContextAdapter.cs
@@ -0,0 +1,45 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Layout
+{
+ internal class LayoutContextAdapter : VirtualizingLayoutContext
+ {
+ private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
+
+ public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
+ {
+ _nonVirtualizingContext = nonVirtualizingContext;
+ }
+
+ protected override object LayoutStateCore
+ {
+ get => _nonVirtualizingContext.LayoutState;
+ set => _nonVirtualizingContext.LayoutState = value;
+ }
+
+ protected override Point LayoutOriginCore
+ {
+ get => default;
+ set
+ {
+ if (value != default)
+ {
+ throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
+ }
+ }
+ }
+
+ protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
+
+ protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
+ protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
+ protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
+ _nonVirtualizingContext.Children[index];
+ protected override void RecycleElementCore(ILayoutable element) { }
+ }
+}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs
index 5d27ba9199..fb6b0dd4c9 100644
--- a/src/Avalonia.Layout/NonVirtualizingLayout.cs
+++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs
@@ -17,30 +17,6 @@ namespace Avalonia.Layout
///
public abstract class NonVirtualizingLayout : AttachedLayout
{
- ///
- public sealed override void InitializeForContext(LayoutContext context)
- {
- InitializeForContextCore((NonVirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override void UninitializeForContext(LayoutContext context)
- {
- UninitializeForContextCore((NonVirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override Size Measure(LayoutContext context, Size availableSize)
- {
- return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
- }
-
- ///
- public sealed override Size Arrange(LayoutContext context, Size finalSize)
- {
- return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
- }
-
///
/// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container.
@@ -49,7 +25,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void InitializeForContextCore(LayoutContext context)
+ protected internal virtual void InitializeForContextCore(LayoutContext context)
{
}
@@ -61,7 +37,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void UninitializeForContextCore(LayoutContext context)
+ protected internal virtual void UninitializeForContextCore(LayoutContext context)
{
}
@@ -83,7 +59,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
///
- protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
+ protected internal abstract Size MeasureOverride(
+ NonVirtualizingLayoutContext context,
+ Size availableSize);
///
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children.
///
/// The actual size that is used after the element is arranged in layout.
- protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
+ protected internal virtual Size ArrangeOverride(
+ NonVirtualizingLayoutContext context,
+ Size finalSize) => finalSize;
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
index d3dec83e9b..cef551f32e 100644
--- a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
+++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
@@ -3,6 +3,8 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+using System.Collections.Generic;
+
namespace Avalonia.Layout
{
///
@@ -10,5 +12,20 @@ namespace Avalonia.Layout
///
public abstract class NonVirtualizingLayoutContext : LayoutContext
{
+ private VirtualizingLayoutContext _contextAdapter;
+
+ ///
+ /// Gets the collection of child controls from the container that provides the context.
+ ///
+ public IReadOnlyList Children => ChildrenCore;
+
+ ///
+ /// Implements the behavior for getting the return value of in a
+ /// derived or custom .
+ ///
+ protected abstract IReadOnlyList ChildrenCore { get; }
+
+ internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
+ _contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
}
}
diff --git a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs
new file mode 100644
index 0000000000..0b730315e1
--- /dev/null
+++ b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Data;
+
+namespace Avalonia.Layout
+{
+ public class NonVirtualizingStackLayout : NonVirtualizingLayout
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ StackLayout.OrientationProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SpacingProperty =
+ StackLayout.SpacingProperty.AddOwner();
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
+ /// direction of the StackLayout's Orientation.
+ ///
+ public double Spacing
+ {
+ get => GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
+
+ protected internal override Size MeasureOverride(
+ NonVirtualizingLayoutContext context,
+ Size availableSize)
+ {
+ var extentU = 0.0;
+ var extentV = 0.0;
+ var childCount = context.Children.Count;
+ var isVertical = Orientation == Orientation.Vertical;
+ var spacing = Spacing;
+ var constraint = isVertical ?
+ availableSize.WithHeight(double.PositiveInfinity) :
+ availableSize.WithWidth(double.PositiveInfinity);
+
+ for (var i = 0; i < childCount; ++i)
+ {
+ var element = context.Children[i];
+
+ if (!element.IsVisible)
+ {
+ continue;
+ }
+
+ element.Measure(constraint);
+
+ if (isVertical)
+ {
+ extentU += element.DesiredSize.Height;
+ extentV = Math.Max(extentV, element.DesiredSize.Width);
+ }
+ else
+ {
+ extentU += element.DesiredSize.Width;
+ extentV = Math.Max(extentV, element.DesiredSize.Height);
+ }
+
+ if (i < childCount - 1)
+ {
+ extentU += spacing;
+ }
+ }
+
+ return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
+ }
+
+ protected internal override Size ArrangeOverride(
+ NonVirtualizingLayoutContext context,
+ Size finalSize)
+ {
+ var u = 0.0;
+ var childCount = context.Children.Count;
+ var isVertical = Orientation == Orientation.Vertical;
+ var spacing = Spacing;
+ var bounds = new Rect();
+
+ for (var i = 0; i < childCount; ++i)
+ {
+ var element = context.Children[i];
+
+ if (!element.IsVisible)
+ {
+ continue;
+ }
+
+ bounds = isVertical ?
+ LayoutVertical(element, u, finalSize) :
+ LayoutHorizontal(element, u, finalSize);
+ element.Arrange(bounds);
+ u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
+ }
+
+ return new Size(bounds.Right, bounds.Bottom);
+ }
+
+ private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)
+ {
+ var x = 0.0;
+ var width = element.DesiredSize.Width;
+
+ switch (element.HorizontalAlignment)
+ {
+ case HorizontalAlignment.Center:
+ x += (constraint.Width - element.DesiredSize.Width) / 2;
+ break;
+ case HorizontalAlignment.Right:
+ x += constraint.Width - element.DesiredSize.Width;
+ break;
+ case HorizontalAlignment.Stretch:
+ width = constraint.Width;
+ break;
+ }
+
+ return new Rect(x, y, width, element.DesiredSize.Height);
+ }
+
+ private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint)
+ {
+ var y = 0.0;
+ var height = element.DesiredSize.Height;
+
+ switch (element.VerticalAlignment)
+ {
+ case VerticalAlignment.Center:
+ y += (constraint.Height - element.DesiredSize.Height) / 2;
+ break;
+ case VerticalAlignment.Bottom:
+ y += constraint.Height - element.DesiredSize.Height;
+ break;
+ case VerticalAlignment.Stretch:
+ height = constraint.Height;
+ break;
+ }
+
+ return new Rect(x, y, element.DesiredSize.Width, height);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
index e8ad49e9b9..9b8eb4814e 100644
--- a/src/Avalonia.Layout/StackLayout.cs
+++ b/src/Avalonia.Layout/StackLayout.cs
@@ -234,7 +234,7 @@ namespace Avalonia.Layout
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
- protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
@@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this);
}
- protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context);
}
- protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
@@ -275,7 +275,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
- protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
index 54c3ccbb90..ee9cff4a01 100644
--- a/src/Avalonia.Layout/UniformGridLayout.cs
+++ b/src/Avalonia.Layout/UniformGridLayout.cs
@@ -392,7 +392,7 @@ namespace Avalonia.Layout
{
}
- protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var gridState = state as UniformGridLayoutState;
@@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this);
}
- protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context);
}
- protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
// Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
@@ -442,7 +442,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
- protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
diff --git a/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
new file mode 100644
index 0000000000..80ccee2114
--- /dev/null
+++ b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
@@ -0,0 +1,42 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Avalonia.Layout
+{
+ public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
+ {
+ private readonly VirtualizingLayoutContext _virtualizingContext;
+ private ChildrenCollection _children;
+
+ public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
+ {
+ _virtualizingContext = virtualizingContext;
+ }
+
+ protected override object LayoutStateCore
+ {
+ get => _virtualizingContext.LayoutState;
+ set => _virtualizingContext.LayoutState = value;
+ }
+
+ protected override IReadOnlyList ChildrenCore =>
+ _children ?? (_children = new ChildrenCollection(_virtualizingContext));
+
+ private class ChildrenCollection : IReadOnlyList
+ {
+ private readonly VirtualizingLayoutContext _context;
+ public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
+ public ILayoutable this[int index] => _context.GetOrCreateElementAt(index);
+ public int Count => _context.ItemCount;
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public IEnumerator GetEnumerator()
+ {
+ for (var i = 0; i < Count; ++i)
+ {
+ yield return this[i];
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs
index 4c601175f3..15c7749dfe 100644
--- a/src/Avalonia.Layout/VirtualizingLayout.cs
+++ b/src/Avalonia.Layout/VirtualizingLayout.cs
@@ -19,30 +19,6 @@ namespace Avalonia.Layout
///
public abstract class VirtualizingLayout : AttachedLayout
{
- ///
- public sealed override void InitializeForContext(LayoutContext context)
- {
- InitializeForContextCore((VirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override void UninitializeForContext(LayoutContext context)
- {
- UninitializeForContextCore((VirtualizingLayoutContext)context);
- }
-
- ///
- public sealed override Size Measure(LayoutContext context, Size availableSize)
- {
- return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
- }
-
- ///
- public sealed override Size Arrange(LayoutContext context, Size finalSize)
- {
- return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
- }
-
///
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
@@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{
}
@@ -82,7 +58,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
///
- protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{
}
@@ -104,7 +80,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
///
- protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+ protected internal abstract Size MeasureOverride(
+ VirtualizingLayoutContext context,
+ Size availableSize);
///
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children.
///
/// The actual size that is used after the element is arranged in layout.
- protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+ protected internal virtual Size ArrangeOverride(
+ VirtualizingLayoutContext context,
+ Size finalSize) => finalSize;
///
/// Notifies the layout when the data collection assigned to the container element (Items)
diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
index 980daec2eb..079b91a90f 100644
--- a/src/Avalonia.Layout/VirtualizingLayoutContext.cs
+++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
@@ -43,6 +43,8 @@ namespace Avalonia.Layout
///
public abstract class VirtualizingLayoutContext : LayoutContext
{
+ private NonVirtualizingLayoutContext _contextAdapter;
+
///
/// Gets the number of items in the data.
///
@@ -186,5 +188,8 @@ namespace Avalonia.Layout
///
/// The element to clear.
protected abstract void RecycleElementCore(ILayoutable element);
+
+ internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
+ _contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
}
}
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 60fd0346a3..478a908951 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -649,7 +649,27 @@ namespace Avalonia.X11
ScheduleInput(args);
}
- public void ScheduleInput(RawInputEventArgs args)
+ public void ScheduleXI2Input(RawInputEventArgs args)
+ {
+ if (args is RawPointerEventArgs pargs)
+ {
+ if ((pargs.Type == RawPointerEventType.TouchBegin
+ || pargs.Type == RawPointerEventType.TouchUpdate
+ || pargs.Type == RawPointerEventType.LeftButtonDown
+ || pargs.Type == RawPointerEventType.RightButtonDown
+ || pargs.Type == RawPointerEventType.MiddleButtonDown
+ || pargs.Type == RawPointerEventType.NonClientLeftButtonDown)
+ && ActivateTransientChildIfNeeded())
+ return;
+ if (pargs.Type == RawPointerEventType.TouchEnd
+ && ActivateTransientChildIfNeeded())
+ pargs.Type = RawPointerEventType.TouchCancel;
+ }
+
+ ScheduleInput(args);
+ }
+
+ private void ScheduleInput(RawInputEventArgs args)
{
if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling;
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index ac14efe133..0734532d92 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
- client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
+ client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
@@ -230,10 +230,10 @@ namespace Avalonia.X11
}
if (scrollDelta != default)
- client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
+ client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev))
- client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
+ client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
@@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null
};
if (type.HasValue)
- client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
+ client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
}
@@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client
{
IInputRoot InputRoot { get; }
- void ScheduleInput(RawInputEventArgs args);
+ void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; }
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 9612fa3d9b..d0157815a9 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -140,25 +140,17 @@ namespace Avalonia.Skia
public Rect HitTestTextPosition(int index)
{
+ if (string.IsNullOrEmpty(Text))
+ {
+ var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
+ return new Rect(alignmentOffset, 0, 0, _lineHeight);
+ }
var rects = GetRects();
-
- if (index < 0 || index >= rects.Count)
+ if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
}
-
- if (rects.Count == 0)
- {
- return new Rect(0, 0, 1, _lineHeight);
- }
-
- if (index == rects.Count)
- {
- var lr = rects[rects.Count - 1];
- return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
- }
-
return rects[index];
}
diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs
index fe626f4d38..abace92f08 100644
--- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs
+++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Win32.Embedding
focused = focused.VisualParent;
if (focused == _root)
- KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None);
+ KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
}
private void PlatformImpl_LostFocus()
diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
index f1123e3958..1258bb0109 100644
--- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
+++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Win32.Input
public void WindowActivated(Window window)
{
- SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None);
+ SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None);
}
public string StringFromVirtualKey(uint virtualKey)
diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
index 05224c2495..5a9ca410e4 100644
--- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
@@ -1,4 +1,6 @@
using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Shapes
@@ -12,5 +14,21 @@ namespace Avalonia.Controls.UnitTests.Shapes
target.Measure(Size.Infinity);
}
+
+ [Fact]
+ public void Subscribes_To_Geometry_Changes()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
+ var target = new Path { Data = geometry };
+
+ target.Measure(Size.Infinity);
+ Assert.True(target.IsMeasureValid);
+
+ geometry.Rect = new Rect(0, 0, 20, 20);
+
+ Assert.False(target.IsMeasureValid);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 6bb3d55799..d2f62cde04 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -1,10 +1,12 @@
using System;
using System.Reactive.Linq;
+using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
+using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
@@ -554,6 +556,34 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Theory]
+ [InlineData(Key.X, KeyModifiers.Control)]
+ [InlineData(Key.Back, KeyModifiers.None)]
+ [InlineData(Key.Delete, KeyModifiers.None)]
+ [InlineData(Key.Tab, KeyModifiers.None)]
+ [InlineData(Key.Enter, KeyModifiers.None)]
+ public void Keys_Allow_Undo(Key key, KeyModifiers modifiers)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "0123",
+ AcceptsReturn = true,
+ AcceptsTab = true
+ };
+ target.SelectionStart = 1;
+ target.SelectionEnd = 3;
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToSingleton();
+
+ RaiseKeyEvent(target, key, modifiers);
+ RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
+ Assert.True(target.Text == "0123");
+ }
+ }
+
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
@@ -616,5 +646,14 @@ namespace Avalonia.Controls.UnitTests
set { _bar = value; RaisePropertyChanged(); }
}
}
+
+ private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
+ {
+ public Task GetTextAsync() => Task.FromResult("");
+
+ public Task SetTextAsync(string text) => Task.CompletedTask;
+
+ public Task ClearAsync() => Task.CompletedTask;
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 32b09e2c47..bd303a81cd 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -1002,6 +1002,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, child2Node.Presenter.Panel.Children.Count);
}
+ [Fact]
+ public void Clearing_TreeView_Items_Clears_Index()
+ {
+ // Issue #3551
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ };
+
+ var root = new TestRoot();
+ root.Child = target;
+
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+
+ var rootNode = tree[0];
+ var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode);
+
+ Assert.NotNull(container);
+
+ root.Child = null;
+
+ tree.Clear();
+
+ Assert.Empty(target.ItemContainerGenerator.Index.Containers);
+ }
+
private void ApplyTemplates(TreeView tree)
{
tree.ApplyTemplate();
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
index 3c8e800fca..df0a077c7f 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
@@ -35,7 +35,7 @@ namespace Avalonia.Input.UnitTests
target.SetFocusedElement(
focused.Object,
NavigationMethod.Unspecified,
- InputModifiers.None);
+ KeyModifiers.None);
target.ProcessRawEvent(
new RawKeyEventArgs(
@@ -75,7 +75,7 @@ namespace Avalonia.Input.UnitTests
target.SetFocusedElement(
focused.Object,
NavigationMethod.Unspecified,
- InputModifiers.None);
+ KeyModifiers.None);
target.ProcessRawEvent(
new RawTextInputEventArgs(
diff --git a/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs
new file mode 100644
index 0000000000..a7b378c322
--- /dev/null
+++ b/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs
@@ -0,0 +1,335 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Layout.UnitTests
+{
+ public class NonVirtualizingStackLayoutTests
+ {
+ [Fact]
+ public void Lays_Out_Children_Vertically()
+ {
+ var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical };
+ var context = CreateContext(new[]
+ {
+ new Border { Height = 20, Width = 120 },
+ new Border { Height = 30 },
+ new Border { Height = 50 },
+ });
+
+ var desiredSize = target.Measure(context, Size.Infinity);
+ var arrangeSize = target.Arrange(context, desiredSize);
+
+ Assert.Equal(new Size(120, 100), desiredSize);
+ Assert.Equal(new Size(120, 100), arrangeSize);
+ Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds);
+ Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Out_Children_Horizontally()
+ {
+ var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal };
+ var context = CreateContext(new[]
+ {
+ new Border { Width = 20, Height = 120 },
+ new Border { Width = 30 },
+ new Border { Width = 50 },
+ });
+
+ var desiredSize = target.Measure(context, Size.Infinity);
+ var arrangeSize = target.Arrange(context, desiredSize);
+
+ Assert.Equal(new Size(100, 120), desiredSize);
+ Assert.Equal(new Size(100, 120), arrangeSize);
+ Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
+ Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds);
+ Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Out_Children_Vertically_With_Spacing()
+ {
+ var target = new NonVirtualizingStackLayout
+ {
+ Orientation = Orientation.Vertical,
+ Spacing = 10,
+ };
+
+ var context = CreateContext(new[]
+ {
+ new Border { Height = 20, Width = 120 },
+ new Border { Height = 30 },
+ new Border { Height = 50 },
+ });
+
+ var desiredSize = target.Measure(context, Size.Infinity);
+ var arrangeSize = target.Arrange(context, desiredSize);
+
+ Assert.Equal(new Size(120, 120), desiredSize);
+ Assert.Equal(new Size(120, 120), arrangeSize);
+ Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds);
+ Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Out_Children_Horizontally_With_Spacing()
+ {
+ var target = new NonVirtualizingStackLayout
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 10,
+ };
+
+ var context = CreateContext(new[]
+ {
+ new Border { Width = 20, Height = 120 },
+ new Border { Width = 30 },
+ new Border { Width = 50 },
+ });
+
+ var desiredSize = target.Measure(context, Size.Infinity);
+ var arrangeSize = target.Arrange(context, desiredSize);
+
+ Assert.Equal(new Size(120, 120), desiredSize);
+ Assert.Equal(new Size(120, 120), arrangeSize);
+ Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
+ Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds);
+ Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Arranges_Vertical_Children_With_Correct_Bounds()
+ {
+ var target = new NonVirtualizingStackLayout
+ {
+ Orientation = Orientation.Vertical
+ };
+
+ var context = CreateContext(new[]
+ {
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ MeasureSize = new Size(50, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ MeasureSize = new Size(150, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ MeasureSize = new Size(50, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ MeasureSize = new Size(150, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Right,
+ MeasureSize = new Size(50, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Right,
+ MeasureSize = new Size(150, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ MeasureSize = new Size(50, 10),
+ },
+ new TestControl
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ MeasureSize = new Size(150, 10),
+ },
+ });
+
+ var desiredSize = target.Measure(context, new Size(100, 150));
+ Assert.Equal(new Size(100, 80), desiredSize);
+
+ target.Arrange(context, desiredSize);
+
+ var bounds = context.Children.Select(x => x.Bounds).ToArray();
+
+ Assert.Equal(
+ new[]
+ {
+ new Rect(0, 0, 50, 10),
+ new Rect(0, 10, 100, 10),
+ new Rect(25, 20, 50, 10),
+ new Rect(0, 30, 100, 10),
+ new Rect(50, 40, 50, 10),
+ new Rect(0, 50, 100, 10),
+ new Rect(0, 60, 100, 10),
+ new Rect(0, 70, 100, 10),
+
+ }, bounds);
+ }
+
+ [Fact]
+ public void Arranges_Horizontal_Children_With_Correct_Bounds()
+ {
+ var target = new NonVirtualizingStackLayout
+ {
+ Orientation = Orientation.Horizontal
+ };
+
+ var context = CreateContext(new[]
+ {
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Top,
+ MeasureSize = new Size(10, 50),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Top,
+ MeasureSize = new Size(10, 150),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ MeasureSize = new Size(10, 50),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ MeasureSize = new Size(10, 150),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Bottom,
+ MeasureSize = new Size(10, 50),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Bottom,
+ MeasureSize = new Size(10, 150),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ MeasureSize = new Size(10, 50),
+ },
+ new TestControl
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ MeasureSize = new Size(10, 150),
+ },
+ });
+
+ var desiredSize = target.Measure(context, new Size(150, 100));
+ Assert.Equal(new Size(80, 100), desiredSize);
+
+ target.Arrange(context, desiredSize);
+
+ var bounds = context.Children.Select(x => x.Bounds).ToArray();
+
+ Assert.Equal(
+ new[]
+ {
+ new Rect(0, 0, 10, 50),
+ new Rect(10, 0, 10, 100),
+ new Rect(20, 25, 10, 50),
+ new Rect(30, 0, 10, 100),
+ new Rect(40, 50, 10, 50),
+ new Rect(50, 0, 10, 100),
+ new Rect(60, 0, 10, 100),
+ new Rect(70, 0, 10, 100),
+ }, bounds);
+ }
+
+ [Theory]
+ [InlineData(Orientation.Horizontal)]
+ [InlineData(Orientation.Vertical)]
+ public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
+ {
+ var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout
+ {
+ Orientation = orientation,
+ Spacing = 40,
+ };
+
+ var contextThreeChildrenOneInvisble = CreateContext(new[]
+ {
+ new StackPanel { Width = 10, Height= 10, IsVisible = false },
+ new StackPanel { Width = 10, Height= 10 },
+ new StackPanel { Width = 10, Height= 10 },
+ });
+
+ var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout
+ {
+ Spacing = 40,
+ Orientation = orientation,
+ };
+
+ var contextTwoChildrenNoneInvisible = CreateContext(new[]
+ {
+ new StackPanel { Width = 10, Height = 10 },
+ new StackPanel { Width = 10, Height = 10 }
+ });
+
+ var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity);
+ var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity);
+
+ Assert.Equal(desiredSize2, desiredSize1);
+ }
+
+ [Theory]
+ [InlineData(Orientation.Horizontal)]
+ [InlineData(Orientation.Vertical)]
+ public void Only_Arrange_Visible_Children(Orientation orientation)
+ {
+ var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
+ var panel = new Panel { Width = 10, Height = 10 };
+
+ var target = new NonVirtualizingStackLayout
+ {
+ Spacing = 40,
+ Orientation = orientation,
+ };
+
+ var context = CreateContext(new[]
+ {
+ hiddenPanel,
+ panel
+ });
+
+ var desiredSize = target.Measure(context, Size.Infinity);
+ var arrangeSize = target.Arrange(context, desiredSize);
+ Assert.Equal(new Size(10, 10), arrangeSize);
+ }
+
+ private NonVirtualizingLayoutContext CreateContext(Control[] children)
+ {
+ return new TestLayoutContext(children);
+ }
+
+ private class TestLayoutContext : NonVirtualizingLayoutContext
+ {
+ public TestLayoutContext(Control[] children) => ChildrenCore = children;
+ protected override IReadOnlyList ChildrenCore { get; }
+ }
+
+ private class TestControl : Control
+ {
+ public Size MeasureConstraint { get; private set; }
+ public Size MeasureSize { get; set; }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ MeasureConstraint = availableSize;
+ return MeasureSize;
+ }
+ }
+ }
+}