diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a5bbcec186..69da211aa4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -240,17 +241,14 @@ namespace Avalonia.Controls.Primitives public override void BeginInit() { base.BeginInit(); - ++_updateCount; - _updateSelectedIndex = int.MinValue; + + InternalBeginInit(); } /// public override void EndInit() { - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); base.EndInit(); } @@ -437,7 +435,8 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_updateCount; + + InternalBeginInit(); } /// @@ -445,10 +444,7 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextEndUpdate(); - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); } protected override void OnKeyDown(KeyEventArgs e) @@ -1118,6 +1114,26 @@ namespace Avalonia.Controls.Primitives } } + private void InternalBeginInit() + { + if (_updateCount == 0) + { + _updateSelectedIndex = int.MinValue; + } + + ++_updateCount; + } + + private void InternalEndInit() + { + Debug.Assert(_updateCount > 0); + + if (--_updateCount == 0) + { + UpdateFinished(); + } + } + private class Selection : IEnumerable { private readonly List _list = new List(); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index e7cd0697a0..4b3e8e2110 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -7,8 +7,14 @@ using Avalonia.Data; namespace Avalonia.Controls.Primitives { + /// + /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. + /// public class ToggleButton : Button { + /// + /// Defines the property. + /// public static readonly DirectProperty IsCheckedProperty = AvaloniaProperty.RegisterDirect( nameof(IsChecked), @@ -17,9 +23,30 @@ namespace Avalonia.Controls.Primitives unsetValue: null, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// public static readonly StyledProperty IsThreeStateProperty = AvaloniaProperty.Register(nameof(IsThreeState)); + /// + /// Defines the event. + /// + public static readonly RoutedEvent CheckedEvent = + RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent UncheckedEvent = + RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IndeterminateEvent = + RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble); + private bool? _isChecked = false; static ToggleButton() @@ -27,14 +54,49 @@ namespace Avalonia.Controls.Primitives PseudoClass(IsCheckedProperty, c => c == true, ":checked"); PseudoClass(IsCheckedProperty, c => c == false, ":unchecked"); PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate"); + + IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); + } + + /// + /// Raised when a is checked. + /// + public event EventHandler Checked + { + add => AddHandler(CheckedEvent, value); + remove => RemoveHandler(CheckedEvent, value); + } + + /// + /// Raised when a is unchecked. + /// + public event EventHandler Unchecked + { + add => AddHandler(UncheckedEvent, value); + remove => RemoveHandler(UncheckedEvent, value); + } + + /// + /// Raised when a is neither checked nor unchecked. + /// + public event EventHandler Indeterminate + { + add => AddHandler(IndeterminateEvent, value); + remove => RemoveHandler(IndeterminateEvent, value); } + /// + /// Gets or sets whether the is checked. + /// public bool? IsChecked { - get { return _isChecked; } - set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + get => _isChecked; + set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + /// + /// Gets or sets a value that indicates whether the control supports three states. + /// public bool IsThreeState { get => GetValue(IsThreeStateProperty); @@ -47,18 +109,78 @@ namespace Avalonia.Controls.Primitives base.OnClick(); } + /// + /// Toggles the property. + /// protected virtual void Toggle() { if (IsChecked.HasValue) + { if (IsChecked.Value) + { if (IsThreeState) + { IsChecked = null; + } else + { IsChecked = false; + } + } else + { IsChecked = true; + } + } else + { IsChecked = false; + } + } + + /// + /// Called when becomes true. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnChecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes false. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnUnchecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes null. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnIndeterminate(RoutedEventArgs e) + { + RaiseEvent(e); + } + + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool?)e.NewValue; + + switch (newValue) + { + case true: + OnChecked(new RoutedEventArgs(CheckedEvent)); + break; + case false: + OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + break; + default: + OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + break; + } } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0e2136a6f3..cbac1d6c1b 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -562,33 +562,35 @@ namespace Avalonia.Controls if (Layout != null) { - if (Layout is VirtualizingLayout virtualLayout) - { - var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + try + { _processingItemsSourceChange = args; - try + if (Layout is VirtualizingLayout virtualLayout) { virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } - finally - { - _processingItemsSourceChange = null; - } - } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) - { - // Walk through all the elements and make sure they are cleared for - // non-virtualizing layouts. - foreach (var element in Children) + else if (Layout is NonVirtualizingLayout nonVirtualLayout) { - if (GetVirtualizationInfo(element).IsRealized) + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) { - ClearElementImpl(element); + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } } + + Children.Clear(); } } + finally + { + _processingItemsSourceChange = null; + } InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 51c14d47d6..7d005a30b4 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -109,11 +109,22 @@ namespace Avalonia.Controls public void ClearElementToElementFactory(IControl element) { - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - _owner.ItemTemplateShim.RecycleElement(_owner, element); + if (_owner.ItemTemplateShim != null) + { + _owner.ItemTemplateShim.RecycleElement(_owner, element); + } + else + { + // No ItemTemplate to recycle to, remove the element from the children collection. + if (!_owner.Children.Remove(element)) + { + throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection."); + } + } + + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToElementFactory(); if (_lastFocusedElement == element) @@ -121,9 +132,8 @@ namespace Avalonia.Controls // Focused element is going away. Remove the tracked last focused element // and pick a reasonable next focus if we can find one within the layout // realized elements. - MoveFocusFromClearedIndex(clearedIndex); + MoveFocusFromClearedIndex(virtInfo.Index); } - } private void MoveFocusFromClearedIndex(int clearedIndex) @@ -190,7 +200,8 @@ namespace Avalonia.Controls { if (virtInfo == null) { - throw new ArgumentException("Element is not a child of this ItemsRepeater."); + //Element is not a child of this ItemsRepeater. + return -1; } return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; @@ -515,21 +526,52 @@ namespace Avalonia.Controls return element; } + // There are several cases handled here with respect to which element gets returned and when DataContext is modified. + // + // 1. If there is no ItemTemplate: + // 1.1 If data is an IControl -> the data is returned + // 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data + // + // 2. If there is an ItemTemplate: + // 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data + // 2.2 If data is an IControl: + // 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is + // 2.2.2 If Element returned by the ElementFactory is not the same as the data + // -> Element that is fetched from the ElementFactory is returned and + // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself private IControl GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. + var data = _owner.ItemsSourceView.GetAt(index); + var providedElementFactory = _owner.ItemTemplateShim; + + ItemTemplateWrapper GetElementFactory() + { + if (providedElementFactory == null) + { + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + return _owner.ItemTemplateShim; + } - var itemTemplateFactory = _owner.ItemTemplateShim; - if (itemTemplateFactory == null) + return providedElementFactory; + } + + IControl GetElement() { - // If no ItemTemplate was provided, use a default - var factory = FuncDataTemplate.Default; - _owner.ItemTemplate = factory; - itemTemplateFactory = _owner.ItemTemplateShim; + if (providedElementFactory == null) + { + if (data is IControl dataAsElement) + { + return dataAsElement; + } + } + + var elementFactory = GetElementFactory(); + return elementFactory.GetElement(_owner, data); } - var data = _owner.ItemsSourceView.GetAt(index); - var element = itemTemplateFactory.GetElement(_owner, data); + var element = GetElement(); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) @@ -537,8 +579,11 @@ namespace Avalonia.Controls virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } - // Prepare the element - element.DataContext = data; + if (data != element) + { + // Prepare the element + element.DataContext = data; + } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..7f44c80a64 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -72,6 +72,7 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, string layoutId) { @@ -94,14 +95,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); } RaiseLineArranged(); @@ -115,10 +116,11 @@ namespace Avalonia.Layout public Size Arrange( Size finalSize, VirtualizingLayoutContext context, + bool isWrapping, LineAlignment lineAlignment, string layoutId) { - ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( Math.Max(finalSize.Width, _lastExtent.Width), @@ -270,6 +272,7 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, string layoutId) { if (anchorIndex != -1) @@ -280,7 +283,7 @@ namespace Avalonia.Layout var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; @@ -301,7 +304,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +342,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); @@ -544,6 +547,7 @@ namespace Avalonia.Layout private void ArrangeVirtualizingLayout( Size finalSize, LineAlignment lineAlignment, + bool isWrapping, string layoutId) { // Walk through the realized elements one line at a time and @@ -563,7 +567,7 @@ namespace Avalonia.Layout if (_orientation.MajorStart(currentBounds) != currentLineOffset) { spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); spaceAtLineStart = _orientation.MinorStart(currentBounds); countInLine = 0; currentLineOffset = _orientation.MajorStart(currentBounds); @@ -580,7 +584,7 @@ namespace Avalonia.Layout if (countInLine > 0) { var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); } } } @@ -594,6 +598,8 @@ namespace Avalonia.Layout double spaceAtLineEnd, double lineSize, LineAlignment lineAlignment, + bool isWrapping, + Size finalSize, string layoutId) { for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) @@ -659,6 +665,14 @@ namespace Avalonia.Layout } bounds = bounds.Translate(-_lastExtent.Position); + + if (!isWrapping) + { + _orientation.SetMinorSize( + ref bounds, + Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize))); + } + var element = _elementManager.GetAt(rangeIndex); element.Arrange(bounds); } diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index fba91e66c7..5d27ba9199 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -20,25 +20,25 @@ namespace Avalonia.Layout /// public sealed override void InitializeForContext(LayoutContext context) { - InitializeForContextCore((VirtualizingLayoutContext)context); + InitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override void UninitializeForContext(LayoutContext context) { - UninitializeForContextCore((VirtualizingLayoutContext)context); + UninitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override Size Measure(LayoutContext context, Size availableSize) { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize); } /// public sealed override Size Arrange(LayoutContext context, Size finalSize) { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize); } /// @@ -49,7 +49,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +61,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +83,7 @@ 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 abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -98,6 +98,6 @@ 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 virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; } } diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs new file mode 100644 index 0000000000..d3dec83e9b --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Avalonia.Layout +{ + /// + /// Represents the base class for layout context types that do not support virtualization. + /// + public abstract class NonVirtualizingLayoutContext : LayoutContext + { + } +} diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31..3c3729272c 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -267,6 +267,7 @@ namespace Avalonia.Layout false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, LayoutId); @@ -278,6 +279,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + false, FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index edc2042922..11a521ed1e 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -110,6 +110,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -123,6 +129,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -219,6 +226,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -269,15 +285,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -299,7 +317,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -329,17 +349,21 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) { _orientation.SetMinorSize( ref extent, - !double.IsInfinity(availableSizeMinor) ? + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? availableSizeMinor : - Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); _orientation.SetMajorSize( ref extent, Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); @@ -398,7 +422,7 @@ namespace Avalonia.Layout // 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. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -406,6 +430,7 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, LayoutId); @@ -421,6 +446,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + true, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, LayoutId); return new Size(value.Width, value.Height); @@ -471,6 +497,10 @@ namespace Avalonia.Layout { _minItemHeight = (double)args.NewValue; } + else if (args.Property == MaximumRowsOrColumnsProperty) + { + _maximumRowsOrColumns = (int)args.NewValue; + } InvalidateLayout(); } @@ -499,7 +529,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d819581000..696c0dbf46 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void SelectedIndex_Should_Be_Minus_1_Without_Initialize() + { + var items = new[] + { + new Item(), + new Item(), + }; + + var target = new ListBox(); + target.Items = items; + target.Template = Template(); + target.DataContext = new object(); + + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs index 4f4ab47b0a..9acd42aba6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -1,5 +1,4 @@ using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests Assert.Null(threeStateButton.IsChecked); } + [Fact] + public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes() + { + var threeStateButton = new ToggleButton(); + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.IsChecked = true; + Assert.True(checkedRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.IsChecked = false; + Assert.True(uncheckedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.IsChecked = null; + Assert.True(indeterminateRaised); + } + + [Fact] + public void ToggleButton_Events_Are_Raised_When_Toggling() + { + var threeStateButton = new TestToggleButton { IsThreeState = true }; + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.Toggle(); + Assert.True(checkedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.Toggle(); + Assert.True(indeterminateRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.Toggle(); + Assert.True(uncheckedRaised); + } + private class Class1 : NotifyingBase { private bool _foo; @@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests set { nullableFoo = value; RaisePropertyChanged(); } } } + + private class TestToggleButton : ToggleButton + { + public new void Toggle() => base.Toggle(); + } } }