diff --git a/src/Uno.UI/DirectUI/DateComparer.cs b/src/Uno.UI/DirectUI/DateComparer.cs index b4ff62154e41..3e4da6ddcdcb 100644 --- a/src/Uno.UI/DirectUI/DateComparer.cs +++ b/src/Uno.UI/DirectUI/DateComparer.cs @@ -103,7 +103,10 @@ private int CompareDate( global::System.Diagnostics.Debug.Assert(m_spCalendar is {}); - long delta = lhs.ToUniversalTime().Ticks - rhs.ToUniversalTime().Ticks; + lhs = lhs.ToUniversalTime(); // UNO + rhs = rhs.ToUniversalTime(); // UNO + + long delta = lhs.Ticks - rhs.Ticks; if (delta < 0) { delta = -delta; diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarLayoutStrategy.uno.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarLayoutStrategy.uno.cs index 5fb37b6618ea..fd2190a1de03 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarLayoutStrategy.uno.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarLayoutStrategy.uno.cs @@ -52,6 +52,24 @@ internal Rect GetElementBounds( return rect; } + internal Rect GetElementArrangeBounds( + ElementType elementType, + int elementIndex, + Rect containerBounds, + Rect windowConstraint, + Size finalSize) + { + GetElementArrangeBoundsImpl( + elementType, + elementIndex, + containerBounds, + windowConstraint, + finalSize, + out var pReturnValue); + + return pReturnValue; + } + internal bool ShouldContinueFillingUpSpace( ElementType elementType, int elementIndex, diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarView.Properties.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarView.Properties.cs index f4091ffa1cd5..730f598e3d02 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarView.Properties.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarView.Properties.cs @@ -777,19 +777,19 @@ private set Windows.UI.Xaml.DependencyProperty.Register( nameof(IsGroupLabelVisible), typeof(bool), typeof(global::Windows.UI.Xaml.Controls.CalendarView), - new FrameworkPropertyMetadata(default(bool))); + new FrameworkPropertyMetadata(false)); public static global::Windows.UI.Xaml.DependencyProperty IsOutOfScopeEnabledProperty { get; } = Windows.UI.Xaml.DependencyProperty.Register( nameof(IsOutOfScopeEnabled), typeof(bool), typeof(global::Windows.UI.Xaml.Controls.CalendarView), - new FrameworkPropertyMetadata(default(bool))); + new FrameworkPropertyMetadata(true)); public static global::Windows.UI.Xaml.DependencyProperty IsTodayHighlightedProperty { get; } = Windows.UI.Xaml.DependencyProperty.Register( nameof(IsTodayHighlighted), typeof(bool), typeof(global::Windows.UI.Xaml.Controls.CalendarView), - new FrameworkPropertyMetadata(default(bool))); + new FrameworkPropertyMetadata(true)); public static global::Windows.UI.Xaml.DependencyProperty MaxDateProperty { get; } = Windows.UI.Xaml.DependencyProperty.Register( diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarViewGeneratorHost.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarViewGeneratorHost.cs index 82e6b6914afc..f3aa1c5c56cb 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarViewGeneratorHost.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/CalendarViewGeneratorHost.cs @@ -160,7 +160,7 @@ internal virtual void PrepareItemContainer( return; } - private void ClearContainerForItem( + internal void ClearContainerForItem( DependencyObject pContainer, object pItem) { @@ -212,7 +212,7 @@ private void ClearGroupContainerForGroup( throw new NotImplementedException(); } - private bool CanRecycleContainer( + internal bool CanRecycleContainer( DependencyObject pContainer) { var pCanRecycleContainer = true; @@ -602,7 +602,7 @@ internal void UpdateScope( internal void AdjustToFirstUnitInThisScope(out DateTime pDate) { int _ = 0; - AdjustToLastUnitInThisScope(out pDate, ref _); + AdjustToFirstUnitInThisScope(out pDate, ref _); } private void AdjustToFirstUnitInThisScope(out DateTime pDate, ref int pUnit /* = null */) @@ -649,7 +649,7 @@ private void AdjustToLastUnitInThisScope(out DateTime pDate, ref int pUnit /* = //if (pUnit) //{ - // pUnit = lastUnit; + pUnit = lastUnit; //} } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs index 842fe0ea7402..3067383701da 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs @@ -1,8 +1,15 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Text; using Windows.Foundation; using Uno; +using Uno.Extensions; +using Uno.Extensions.Specialized; +using Uno.UI; namespace Windows.UI.Xaml.Controls.Primitives { @@ -10,179 +17,343 @@ namespace Windows.UI.Xaml.Controls.Primitives partial class CalendarPanel : ILayoutDataInfoProvider { - internal class ContainersCache : IItemContainerMapping + // The CalendarView has a minimum size of 296x350, any size under this one will trigger clipping + // TODO: Is this size updated according to accessibility font scale factor? + private static readonly Size _defaultHardCodedSize = new Size(296, 350 - 78); // 78 px for the header etc. + + // Minimum item/cell size to trigger a full measure pass. + // Below this threshold, we only make sure to insert the first item in the Children collection to allow valid computation of the DetermineTheBiggestItemSize. + private static readonly Size _minCellSize = new Size(10, 10); + + private class ContainersCache : IItemContainerMapping { - private readonly Dictionary _byIndex = new Dictionary(); - private readonly Dictionary _byItem = new Dictionary(); - private readonly Dictionary _byContainer = new Dictionary(); + private readonly List _entries = new List(31 + 7 * 2); // A month + one week before and after + private CalendarViewGeneratorHost? _host; - private CalendarViewGeneratorHost _host; + private int _generationStartIndex = -1; + private int _generationCurrentIndex = -1; + private int _generationEndIndex = -1; + private GenerationState _generationState; + private (int at, int count) _generationRecyclableBefore; + private (int at, int count) _generationRecyclableAfter; - internal CalendarViewGeneratorHost Host + internal CalendarViewGeneratorHost? Host { get => _host; set { _host = value; - _byIndex.Clear(); - _byIndex.Clear(); - _byContainer.Clear(); + _entries.Clear(); } } - internal (UIElement container, bool isNew) GetOrCreate(int index) + internal int StartIndex { get; private set; } = -1; + + internal int EndIndex { get; private set; } = -1; + + private bool IsInRange(int itemIndex) + => itemIndex >= StartIndex && itemIndex <= EndIndex; + + private int GetEntryIndex(int itemIndex) + => itemIndex - StartIndex; + + private enum GenerationState { - if (_byIndex.TryGetValue(index, out var cached)) + Before, + InRange, + After + } + + internal void BeginGeneration(int startIndex, int endIndex) + { + if (_host is null) { - return (cached.container, false); + throw new InvalidOperationException("Host not set yet"); } - else if (_host is null) + //Debug.Assert(_recyclableEntries is null); + Debug.Assert(_generationStartIndex == -1); + Debug.Assert(_generationCurrentIndex == -1); + Debug.Assert(_generationEndIndex == -1); + + //_recyclableEntries = new Queue(_entries.Count); + + _generationStartIndex = startIndex; + _generationCurrentIndex = startIndex; + _generationEndIndex = endIndex; + _generationState = GenerationState.Before; + + // Note: Start and End indexes are INCLUSIVE + startIndex = Math.Max(StartIndex, startIndex); + endIndex = Math.Min(EndIndex, endIndex); + + if (endIndex < 0) { - throw new InvalidOperationException("Host not set."); + return; // Cache is empty } - else - { - var item = _host[index]; - var container = (UIElement)_host.GetContainerForItem(item, null); - _byIndex[index] = (item, container); - _byItem[item] = (index, container); - _byContainer[container] = (index, item); + var startEntryIndex = Math.Min(GetEntryIndex(startIndex), _entries.Count); + var endEntryIndex = Math.Max(0, GetEntryIndex(endIndex) + 1); - _host.PrepareItemContainer(container, item); + _generationRecyclableBefore = (0, startEntryIndex); + _generationRecyclableAfter = (endEntryIndex, Math.Max(0, _entries.Count - endEntryIndex)); - return (container, true); - } + Debug.Assert( + (_generationRecyclableAfter.at == _entries.Count && _generationRecyclableAfter.count == 0) // Nothing to recycle at the end + || (_generationRecyclableAfter.at + _generationRecyclableAfter.count == _entries.Count)); // The last recycle item does exists! } - /// - public object ItemFromContainer(DependencyObject container) - => container is UIElement elt && _byContainer.TryGetValue(elt, out var cached) ? cached.item : default; + internal IEnumerable CompleteGeneration(int endIndex) + { + //Debug.Assert(_recyclableEntries is { }); + Debug.Assert(_generationCurrentIndex - 1 == endIndex); // endIndex is inclusive while _generationCurrentIndex is the next index to use - /// - public DependencyObject ContainerFromItem(object item) - => _byItem.TryGetValue(item, out var cached) ? cached.container : default; + // Since the _generationEndIndex is only an estimation, we might have some items that was not flagged as recyclable which were not used. + var unexpectedUnusedEntries = (at: -1, count: Math.Max(0, _generationEndIndex - endIndex)); + if (unexpectedUnusedEntries.count > 0) + { + // If actually all entries have been recycled, some entries might be part of the recyclable head. + unexpectedUnusedEntries.at = Math.Max(_generationRecyclableBefore.at, _generationRecyclableAfter.at - unexpectedUnusedEntries.count); + unexpectedUnusedEntries.count = Math.Min(unexpectedUnusedEntries.count, _entries.Count - unexpectedUnusedEntries.at); + } - /// - public int IndexFromContainer(DependencyObject container) - => container is UIElement elt && _byContainer.TryGetValue(elt, out var cached) ? cached.index : default; + var unusedEntriesCount = _generationRecyclableBefore.count + + _generationRecyclableAfter.count + + unexpectedUnusedEntries.count; - /// - public DependencyObject ContainerFromIndex(int index) - => _byIndex.TryGetValue(index, out var cached) ? cached.container : default; - } + IEnumerable unusedEntries; + if (unusedEntriesCount > 0) + { + var removedEntries = new CacheEntry[unusedEntriesCount]; + var removed = 0; - internal event VisibleIndicesUpdatedEventCallback VisibleIndicesUpdated; + // We need to process from the end to the begin in order to not alter indexes: + // ..Recycled..Recyclable-Head..In-Range..Unexpected-Remaining-Items..Recyclable-Tail..Recycled.. - private readonly ContainersCache _cache = new ContainersCache(); - private CalendarLayoutStrategy _layoutStrategy; - private CalendarViewGeneratorHost _host; + if (_generationRecyclableAfter.count > 0) + { + _entries.CopyTo(_generationRecyclableAfter.at, removedEntries, removed, _generationRecyclableAfter.count); + _entries.RemoveRange(_generationRecyclableAfter.at, _generationRecyclableAfter.count); //TODO: Move to a second recycling stage instead of throwing them away. - internal void RegisterItemsHost(CalendarViewGeneratorHost pHost) - { - _host = pHost; - _cache.Host = pHost; - ContainerManager.Host = pHost; - } + removed += _generationRecyclableAfter.count; + } - internal void DisconnectItemsHost() - => RegisterItemsHost(null); + if (unexpectedUnusedEntries.count > 0) + { + _entries.CopyTo(unexpectedUnusedEntries.at, removedEntries, removed, unexpectedUnusedEntries.count); + _entries.RemoveRange(unexpectedUnusedEntries.at, unexpectedUnusedEntries.count); //TODO: Move to a second recycling stage instead of throwing them away. - internal int FirstVisibleIndexBase { get; private set; } - internal int LastVisibleIndexBase { get; private set; } - internal int FirstCacheIndexBase { get; private set; } - internal int LastCacheIndexBase { get; private set; } + removed += unexpectedUnusedEntries.count; + } - [NotImplemented] - internal PanelScrollingDirection PanningDirectionBase { get; } = PanelScrollingDirection.None; + if (_generationRecyclableBefore.count > 0) + { + _entries.CopyTo(_generationRecyclableBefore.at, removedEntries, removed, _generationRecyclableBefore.count); + _entries.RemoveRange(_generationRecyclableBefore.at, _generationRecyclableBefore.count); //TODO: Move to a second recycling stage instead of throwing them away. - internal DependencyObject ContainerFromIndex(int index) - => _cache.GetOrCreate(index).container; + removed += _generationRecyclableBefore.count; + } - private Size base_MeasureOverride(Size availableSize) - { - if (_host is null || _layoutStrategy is null) - { - return default; - } + Debug.Assert(removed == unusedEntriesCount); - // GOTO TO TODAY: EstimateElementIndex + unusedEntries = removedEntries; + } + else + { + Debug.Assert(unusedEntriesCount == 0); - _layoutStrategy.BeginMeasure(); - ShouldInterceptInvalidate = true; - try + unusedEntries = Enumerable.Empty(); + } + + _entries.Sort(CacheEntryComparer.Instance); + + StartIndex = _entries[0].Index; + EndIndex = _entries[_entries.Count - 1].Index; + + Debug.Assert(_generationStartIndex == StartIndex); + Debug.Assert(endIndex == EndIndex); + Debug.Assert(StartIndex + _entries.Count - 1 == EndIndex); + Debug.Assert(_entries.Skip(1).Select((e, i) => _entries[i].Index + 1 == e.Index).AllTrue()); + + _generationStartIndex = -1; + _generationCurrentIndex = -1; + _generationEndIndex = -1; + + return unusedEntries; + } + + internal (UIElement container, bool isNew) GetOrCreate(int index) { - var index = -1; - var count = _host.Count; - var layout = new LayoutReference{RelativeLocation = ReferenceIdentity.Myself}; - var window = new Rect(default, availableSize); + Debug.Assert(_host is { }); + Debug.Assert(_generationStartIndex <= index); + Debug.Assert(_generationCurrentIndex == index); + // We do not validate Debug.Assert(_generationEndIndex >= index); as the generationEndIndex is only an estimate - while ( - ++index < count - && _layoutStrategy.ShouldContinueFillingUpSpace(ElementType.ItemContainer, index, layout, window)) + _generationCurrentIndex++; + + switch (_generationState) { - var (container, isNew) = _cache.GetOrCreate(index); - if (isNew || !Children.Contains(container)) // TODO: Our Children are being altered, we cannot trust the isNew! :@ + case GenerationState.Before when index >= StartIndex: + if (index > EndIndex) + { + _generationState = GenerationState.After; + goto after; + } + else + { + _generationState = GenerationState.InRange; + goto inRange; + } + case GenerationState.InRange when index > EndIndex + || GetEntryIndex(index) >= _generationRecyclableAfter.at + _generationRecyclableAfter.count: // Unfortunately we had already recycled that container, we need to create a new one! + _generationState = GenerationState.After; + goto after; + + case GenerationState.InRange: + inRange: { - Children.Add((UIElement)container); - } - var itemsSize = _layoutStrategy.GetElementMeasureSize(ElementType.ItemContainer, index, window); - var itemBounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index, itemsSize, layout, window); + var entryIndex = GetEntryIndex(index); + var entry = _entries[entryIndex]; - container.Measure(itemsSize); - container.GetVirtualizationInformation().MeasureSize = itemsSize; + if (entryIndex == _generationRecyclableAfter.at && _generationRecyclableAfter.count > 0) + { + // Finally a container which was eligible for recycling is still valid ... we saved it in extremis! + _generationRecyclableAfter.at++; + _generationRecyclableAfter.count--; + } - layout.RelativeLocation = ReferenceIdentity.AfterMe; - layout.ReferenceBounds = itemBounds; + Debug.Assert(entry.Index == index); + + return (entry.Container, false); + } + + case GenerationState.Before: + case GenerationState.After: + after: + { + var item = _host![index]; + + CacheEntry entry; + bool isNew; + if (_generationRecyclableBefore.count > 0) + { + entry = _entries[_generationRecyclableBefore.at]; + isNew = false; + + _generationRecyclableBefore.at++; + _generationRecyclableBefore.count--; + } + else if (_generationRecyclableAfter.count > 0) + { + entry = _entries[_generationRecyclableAfter.at + _generationRecyclableAfter.count - 1]; + isNew = false; + + _generationRecyclableAfter.count--; + + Debug.Assert(entry.Index > index); + } + else + { + var container = (UIElement)_host.GetContainerForItem(item, null); + entry = new CacheEntry(container); + isNew = true; + + _entries.Add(entry); + } + + entry.Index = index; + entry.Item = item; + + _host.PrepareItemContainer(entry.Container, item); + + return (entry.Container, isNew); + } } - StartIndex = 0; - FirstVisibleIndexBase = 0; - LastVisibleIndexBase = index; + throw new InvalidOperationException("Non reachable case."); } - finally + + /// + public object? ItemFromContainer(DependencyObject container) + => container is UIElement elt ? _entries.Find(e => e.Container == elt)?.Container : default; + + /// + public DependencyObject? ContainerFromItem(object item) + => _entries.Find(e => e.Item == item)?.Container; + + /// + public int IndexFromContainer(DependencyObject container) + => container is UIElement elt ? _entries.Find(e => e.Container == elt)?.Index ?? default : default; + + /// + public DependencyObject? ContainerFromIndex(int index) + => IsInRange(index) ? _entries[GetEntryIndex(index)].Container : default; + } + + private class CacheEntry + { + public CacheEntry(UIElement container) { - ShouldInterceptInvalidate = false; - _layoutStrategy.EndMeasure(); + Container = container; } - VisibleIndicesUpdated?.Invoke(this, null); - //foreach (var item in _host) - //{ + public UIElement Container { get; } - //} + public int Index { get; set; } - //var s = _layoutStrategy.GetDesiredViewportSize(); + public object? Item { get; set; } + } + private class CacheEntryComparer : IComparer + { + public static CacheEntryComparer Instance { get; } = new CacheEntryComparer(); + public int Compare(CacheEntry x, CacheEntry y) => x.Index.CompareTo(y.Index); + } - //return s; + internal event VisibleIndicesUpdatedEventCallback VisibleIndicesUpdated; - _layoutStrategy.EstimatePanelExtent( - default /* not used by CalendarLayoutStrategyImpl */, - default /* not used by CalendarLayoutStrategyImpl */, - default /* not used by CalendarLayoutStrategyImpl */, - out var desiredSize); + private readonly ContainersCache _cache = new ContainersCache(); + private CalendarLayoutStrategy? _layoutStrategy; + private CalendarViewGeneratorHost? _host; + private Rect _effectiveViewport; + private Rect _lastLayoutedViewport = Rect.Empty; - return desiredSize; + private void base_Initialize() + { + ContainerManager = new ContainerManager(this); + VerticalAlignment = VerticalAlignment.Top; + HorizontalAlignment = HorizontalAlignment.Left; + EffectiveViewportChanged += OnEffectiveViewportChanged; } - private Size base_ArrangeOverride(Size finalSize) - { - var layout = new LayoutReference(); // Empty layout which will actually drive the ShouldContinueFillingUpSpace to always return true - var window = new Rect(default, finalSize); + #region Private and internal API required by UWP code + internal int FirstVisibleIndexBase { get; private set; } + internal int LastVisibleIndexBase { get; private set; } + internal int FirstCacheIndexBase { get; private set; } + internal int LastCacheIndexBase { get; private set; } - foreach (var child in Children) - { - var index = _cache.IndexFromContainer(child); - var bounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index, child.DesiredSize, layout, window); + [NotImplemented] + internal PanelScrollingDirection PanningDirectionBase { get; } = PanelScrollingDirection.None; - child.Arrange(bounds); - child.GetVirtualizationInformation().Bounds = bounds; - } + internal ILayoutStrategy? LayoutStrategy => _layoutStrategy; - return finalSize; + internal double CacheLengthBase { get; set; } + + internal ContainerManager ContainerManager { get; private set; } + + internal void RegisterItemsHost(CalendarViewGeneratorHost? pHost) + { + _host = pHost; + _cache.Host = pHost; + Children.Clear(); + ContainerManager.Host = pHost; } + internal void DisconnectItemsHost() + => RegisterItemsHost(null); + + internal DependencyObject? ContainerFromIndex(int index) + => _cache.ContainerFromIndex(index); + [NotImplemented] internal void ScrollItemintoView(int index, ScrollIntoViewAlignment alignment, double offset, bool forceSynchronous) { @@ -195,7 +366,7 @@ internal void ScrollItemIntoView(int index, ScrollIntoViewAlignment alignment, d private Size GetViewportSize() { - return new Size(300,300); + return _lastLayoutedViewport.Size.AtLeast(_defaultHardCodedSize).FiniteOrDefault(_defaultHardCodedSize); } internal Size GetDesiredViewportSize() @@ -224,12 +395,6 @@ internal IItemContainerMapping GetItemContainerMapping() throw new NotImplementedException(); } - internal ILayoutStrategy LayoutStrategy => _layoutStrategy; - - internal double CacheLengthBase { get; set; } - - internal ContainerManager ContainerManager { get; } = new ContainerManager(); - private void SetLayoutStrategyBase(CalendarLayoutStrategy spLayoutStrategy) { _layoutStrategy = spLayoutStrategy; @@ -251,18 +416,177 @@ int ILayoutDataInfoProvider.GetTotalItemCount() /// int ILayoutDataInfoProvider.GetTotalGroupCount() => ContainerManager.TotalGroupCount; + #endregion + + #region Panel / base class (i.e. ModernCollectionBasePanel) implementation (Measure/Arrange) + private Size base_MeasureOverride(Size availableSize) + { + if (_host is null || _layoutStrategy is null) + { + return default; + } + + _layoutStrategy.BeginMeasure(); + ShouldInterceptInvalidate = true; + var index = -1; + try + { + var size = _effectiveViewport.Size.AtLeast(availableSize).AtLeast(_defaultHardCodedSize).FiniteOrDefault(_defaultHardCodedSize); + var position = _effectiveViewport.Location.FiniteOrDefault(default); + var viewport = new Rect(position, size); + + // Gets the index of the first element to render and the actual viewport to use + _layoutStrategy.EstimateElementIndex(ElementType.ItemContainer, default, default, viewport, out var renderWindow, out var startIndex); + renderWindow.Size = viewport.Size; // The actualViewport contains only position information + + var expectedItemsCount = LastVisibleIndex - FirstVisibleIndex; + _cache.BeginGeneration(startIndex, startIndex + expectedItemsCount); + + index = startIndex; + int firstVisibleIndex = -1, lastVisibleIndex = -1; + var count = _host.Count; + var layout = new LayoutReference { RelativeLocation = ReferenceIdentity.Myself }; + var remainingWindowToFill = renderWindow; + + while ( + index < count + && + // _layoutStrategy.ShouldContinueFillingUpSpace only considers items on a single line, + // so we enumerate until we reach the bottom of the viewport, + (layout.ReferenceBounds.Bottom < renderWindow.Bottom + // then we ask to the _layoutStrategy to get items to fill the last line + || _layoutStrategy.ShouldContinueFillingUpSpace(ElementType.ItemContainer, index, layout, remainingWindowToFill)) + ) + { + var (container, isNew) = _cache.GetOrCreate(index); + if (isNew) + { + Children.Add(container); + } + + var itemSize = _layoutStrategy.GetElementMeasureSize(ElementType.ItemContainer, index, renderWindow); // Note: It's actually the same for all items + var itemBounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index, itemSize, layout, renderWindow); + + if (itemSize.Width < _minCellSize.Width && itemSize.Height < _minCellSize.Height) + { + // We don't have any valid cell size yet (This measure pass has been caused by DetermineTheBiggestItemSize), + // so we stop right after having inserted the first child in the Children collection. + index++; + return default; + } + + container.Measure(itemSize); + container.GetVirtualizationInformation().MeasureSize = itemSize; + + var isVisible = viewport.Contains(itemBounds.Location); + if (firstVisibleIndex == -1 && isVisible) + { + firstVisibleIndex = index; + lastVisibleIndex = index; + } + else if (isVisible) + { + lastVisibleIndex = index; + } + + layout.RelativeLocation = ReferenceIdentity.AfterMe; + layout.ReferenceBounds = itemBounds; + remainingWindowToFill.Y = Math.Min(renderWindow.Bottom, itemBounds.Y); + remainingWindowToFill.Height = Math.Max(0, renderWindow.Bottom - itemBounds.Bottom); + + index++; + } + + StartIndex = 0; + FirstVisibleIndexBase = Math.Max(firstVisibleIndex, startIndex); + LastVisibleIndexBase = Math.Max(FirstVisibleIndexBase, lastVisibleIndex); + _lastLayoutedViewport = viewport; + } + finally + { + foreach (var unusedEntry in _cache.CompleteGeneration(index - 1)) + { + Children.Remove(unusedEntry.Container); + } + + Debug.Assert(_cache.StartIndex <= FirstVisibleIndex); + Debug.Assert(_cache.EndIndex >= LastVisibleIndex); + + ShouldInterceptInvalidate = false; + _layoutStrategy.EndMeasure(); + } + VisibleIndicesUpdated?.Invoke(this, null); + + _layoutStrategy.EstimatePanelExtent( + default /* not used by CalendarLayoutStrategyImpl */, + default /* not used by CalendarLayoutStrategyImpl */, + default /* not used by CalendarLayoutStrategyImpl */, + out var desiredSize); + + return desiredSize; + } + + private Size base_ArrangeOverride(Size finalSize) + { + if (_host is null || _layoutStrategy is null) + { + return default; + } + + var layout = new LayoutReference(); // Empty layout which will actually drive the ShouldContinueFillingUpSpace to always return true + var window = new Rect(default, finalSize); + + foreach (var child in Children) + { + var index = _cache.IndexFromContainer(child); + var bounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index, child.DesiredSize, layout, window); + + //TODO _layoutStrategy.GetElementArrangeBounds() + + child.Arrange(bounds); + child.GetVirtualizationInformation().Bounds = bounds; + } + + return finalSize; + } + #endregion + + private static void OnEffectiveViewportChanged(FrameworkElement sender, EffectiveViewportChangedEventArgs args) + { + if (sender is CalendarPanel that) + { + that._effectiveViewport = args.EffectiveViewport; + + if (that._host is null || that._layoutStrategy is null) + { + return; + } + + if (Math.Abs(that._effectiveViewport.Y - that._lastLayoutedViewport.Y) > 100) + { + that.InvalidateMeasure(); + } + } + } } internal class ContainerManager { // Required properties from WinUI code - public int StartOfContainerVisualSection() => 0; + public int StartOfContainerVisualSection() => _owner.FirstVisibleIndex; public int TotalItemsCount => Host?.Count ?? 0; public int TotalGroupCount = 0; // Uno only - public CalendarViewGeneratorHost Host { get; set; } + private readonly CalendarPanel _owner; + + public CalendarViewGeneratorHost? Host { get; set; } + + public ContainerManager(CalendarPanel owner) + { + _owner = owner; + } } } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.uno.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.uno.cs index 5815d82c189b..54267b2903d7 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.uno.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.uno.cs @@ -6,6 +6,8 @@ namespace Windows.UI.Xaml.Controls.Primitives { partial class CalendarPanel { - + /// + internal override bool IsViewHit() + => true; } } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel_Partial.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel_Partial.cs index 23ab08dd8425..5b72191622a1 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel_Partial.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel_Partial.cs @@ -19,6 +19,7 @@ private void Initialize() // Initalize the base class first. // base.Initialize(); + base_Initialize(); spCalendarLayoutStrategy = new CalendarLayoutStrategy(); SetLayoutStrategyBase(spCalendarLayoutStrategy); @@ -374,7 +375,7 @@ private void DetermineTheBiggestItemSize( { Size ignored = default; // no children yet, call base.MeasureOverride to generate at least one anchor item - ignored = base.MeasureOverride(availableSize); + ignored = base_MeasureOverride(availableSize); spChildAsIDO = ContainerFromIndex(ContainerManager.StartOfContainerVisualSection()); Debug.Assert(spChildAsIDO is {}); diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.MuxInternal.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.MuxInternal.cs index 1cbb381e2ef1..b9d6300aefda 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.MuxInternal.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.MuxInternal.cs @@ -1,22 +1,56 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Text; using Windows.UI.Xaml.Automation.Peers; using DirectUI; +using Uno.Disposables; +using Uno.UI.DataBinding; +using Uno.UI.Xaml.Controls; namespace Windows.UI.Xaml.Controls { partial class ScrollViewer { + private IDisposable? _directManipulationHandlerSubscription; + internal bool m_templatedParentHandlesMouseButton; internal bool IsInDirectManipulation { get; } internal bool TemplatedParentHandlesScrolling { get; set; } internal Func AutomationPeerFactoryIndex { get; set; } - internal void SetDirectManipulationStateChangeHandler(IDirectManipulationStateChangeHandler handler) + internal void SetDirectManipulationStateChangeHandler(IDirectManipulationStateChangeHandler? handler) { - + _directManipulationHandlerSubscription?.Dispose(); + + if (handler is null) + { + return; + } + + var weakHandler = WeakReferencePool.RentWeakReference(this, handler); + UpdatesMode = ScrollViewerUpdatesMode.Synchronous; + ViewChanged += OnViewChanged; + _directManipulationHandlerSubscription = Disposable.Create(() => + { + ViewChanged -= OnViewChanged; + WeakReferencePool.ReturnWeakReference(this, weakHandler); + }); + + void OnViewChanged(object sender, ScrollViewerViewChangedEventArgs args) + { + if (args.IsIntermediate) + { + return; + } + + if (weakHandler.Target is IDirectManipulationStateChangeHandler h) + { + h.NotifyStateChange(DMManipulationState.DMManipulationCompleted, default, default, default, default, default, default, default, default); + } + } } }