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; + } + } + } +}