/
CalendarPanel.ModernCollectionBasePanel.cs
779 lines (624 loc) · 25.9 KB
/
CalendarPanel.ModernCollectionBasePanel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Core;
using Uno;
using Uno.Extensions;
using Uno.Extensions.Specialized;
using Uno.Logging;
using Uno.UI;
using Uno.UI.Extensions;
namespace Windows.UI.Xaml.Controls.Primitives
{
// This file is aimed to implement methods that should be implemented by the ModernCollectionBasePanel which is not present in Uno
partial class CalendarPanel : ILayoutDataInfoProvider, ICustomScrollInfo
{
// 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, IEnumerable<CacheEntry>
{
private readonly List<CacheEntry> _entries = new List<CacheEntry>(31 + 7 * 2); // A month + one week before and after
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) _generationUnusedInRange;
private (int at, int count) _generationRecyclableAfter;
internal CalendarViewGeneratorHost? Host
{
get => _host;
set
{
_host = value;
Clear();
}
}
internal int FirstIndex { get; private set; } = -1;
// MinValue: We make sure to have the LastIndex lower than FirstIndex so enumerating from FirstIndex to i ** <= ** LastIndex
// (like in ForeachChildInPanel) we make sure to not consider -1 as a valid index.
internal int LastIndex { get; private set; } = int.MinValue;
private bool IsInRange(int itemIndex)
=> itemIndex >= FirstIndex && itemIndex <= LastIndex;
private int GetEntryIndex(int itemIndex)
=> itemIndex - FirstIndex;
private enum GenerationState
{
Before,
InRange,
After
}
internal void Clear()
{
_entries.Clear();
FirstIndex = -1;
LastIndex = int.MinValue;
_generationStartIndex = -1;
_generationCurrentIndex = -1;
_generationEndIndex = -1;
}
internal void BeginGeneration(int startIndex, int endIndex)
{
if (_host is null)
{
throw new InvalidOperationException("Host not set yet");
}
global::System.Diagnostics.Debug.Assert(_generationStartIndex == -1);
global::System.Diagnostics.Debug.Assert(_generationCurrentIndex == -1);
global::System.Diagnostics.Debug.Assert(_generationEndIndex == -1);
_generationStartIndex = startIndex;
_generationCurrentIndex = startIndex;
_generationEndIndex = endIndex;
_generationState = GenerationState.Before;
// Note: Fist and Last indexes are INCLUSIVE
startIndex = Math.Max(FirstIndex, startIndex);
endIndex = Math.Min(LastIndex, endIndex);
if (endIndex < 0)
{
return; // Cache is empty
}
var startEntryIndex = Math.Min(GetEntryIndex(startIndex), _entries.Count);
var endEntryIndex = Math.Max(0, GetEntryIndex(endIndex) + 1);
// Since the _generationEndIndex is only an estimation, we might have some items that was not flagged as recyclable which are not going to be not used.
// The easiest solution is to track them using the _generationUnusedInRange.
_generationRecyclableBefore = (0, startEntryIndex);
_generationUnusedInRange = (startEntryIndex, endEntryIndex - startEntryIndex);
_generationRecyclableAfter = (endEntryIndex, Math.Max(0, _entries.Count - endEntryIndex));
global::System.Diagnostics.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!
}
internal IEnumerable<CacheEntry> CompleteGeneration(int endIndex)
{
global::System.Diagnostics.Debug.Assert(_generationCurrentIndex - 1 == endIndex); // endIndex is inclusive while _generationCurrentIndex is the next index to use
var unusedEntriesCount = _generationRecyclableBefore.count
+ _generationUnusedInRange.count
+ _generationRecyclableAfter.count;
IEnumerable<CacheEntry> unusedEntries;
if (unusedEntriesCount > 0)
{
var removedEntries = new CacheEntry[unusedEntriesCount];
var removed = 0;
// 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..
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.
removed += _generationRecyclableAfter.count;
}
if (_generationUnusedInRange.count > 0)
{
_entries.CopyTo(_generationUnusedInRange.at, removedEntries, removed, _generationUnusedInRange.count);
_entries.RemoveRange(_generationUnusedInRange.at, _generationUnusedInRange.count); //TODO: Move to a second recycling stage instead of throwing them away.
removed += _generationUnusedInRange.count;
}
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.
removed += _generationRecyclableBefore.count;
}
global::System.Diagnostics.Debug.Assert(removed == unusedEntriesCount);
unusedEntries = removedEntries;
}
else
{
unusedEntries = Enumerable.Empty<CacheEntry>();
}
_entries.Sort(CacheEntryComparer.Instance);
FirstIndex = _entries[0].Index;
LastIndex = _entries[_entries.Count - 1].Index;
global::System.Diagnostics.Debug.Assert(_generationStartIndex == FirstIndex);
global::System.Diagnostics.Debug.Assert(endIndex == LastIndex);
global::System.Diagnostics.Debug.Assert(FirstIndex + _entries.Count - 1 == LastIndex);
global::System.Diagnostics.Debug.Assert(_entries.Skip(1).Select((e, i) => _entries[i].Index + 1 == e.Index).AllTrue());
_generationStartIndex = -1;
_generationCurrentIndex = -1;
_generationEndIndex = -1;
return unusedEntries;
}
internal (CacheEntry entry, CacheEntryKind kind) GetOrCreate(int index)
{
global::System.Diagnostics.Debug.Assert(_host is { });
global::System.Diagnostics.Debug.Assert(_generationStartIndex <= index);
global::System.Diagnostics.Debug.Assert(_generationCurrentIndex == index);
// We do not validate global::System.Diagnostics.Debug.Assert(_generationEndIndex >= index); as the generationEndIndex is only an estimate
_generationCurrentIndex++;
switch (_generationState)
{
case GenerationState.Before when index >= FirstIndex:
if (index > LastIndex)
{
_generationState = GenerationState.After;
goto after;
}
else
{
_generationState = GenerationState.InRange;
goto inRange;
}
case GenerationState.InRange when index > LastIndex
|| 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:
{
var entryIndex = GetEntryIndex(index);
var entry = _entries[entryIndex];
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--;
}
else
{
_generationUnusedInRange.at++;
_generationUnusedInRange.count--;
}
global::System.Diagnostics.Debug.Assert(entry.Index == index);
return (entry, CacheEntryKind.Kept);
}
case GenerationState.Before:
case GenerationState.After:
after:
{
var item = _host![index];
CacheEntry entry;
CacheEntryKind kind;
if (_generationRecyclableBefore.count > 0)
{
entry = _entries[_generationRecyclableBefore.at];
kind = CacheEntryKind.Recycled;
_generationRecyclableBefore.at++;
_generationRecyclableBefore.count--;
}
else if (_generationRecyclableAfter.count > 0)
{
entry = _entries[_generationRecyclableAfter.at + _generationRecyclableAfter.count - 1];
kind = CacheEntryKind.Recycled;
_generationRecyclableAfter.count--;
global::System.Diagnostics.Debug.Assert(entry.Index > index);
}
else
{
var container = (UIElement)_host.GetContainerForItem(item, null);
entry = new CacheEntry(container);
kind = CacheEntryKind.New;
_entries.Add(entry);
}
entry.Index = index;
entry.Item = item;
_host.PrepareItemContainer(entry.Container, item);
return (entry, kind);
}
}
throw new InvalidOperationException("Non reachable case.");
}
/// <inheritdoc />
public object? ItemFromContainer(DependencyObject container)
=> container is UIElement elt ? _entries.Find(e => e.Container == elt)?.Container : default;
/// <inheritdoc />
public DependencyObject? ContainerFromItem(object item)
=> _entries.Find(e => e.Item == item)?.Container;
/// <inheritdoc />
public int IndexFromContainer(DependencyObject container)
=> container is UIElement elt ? _entries.Find(e => e.Container == elt)?.Index ?? -1 : -1;
/// <inheritdoc />
public DependencyObject? ContainerFromIndex(int index)
=> index >= 0 && IsInRange(index) ? _entries[GetEntryIndex(index)].Container : default;
/// <inheritdoc />
public IEnumerator<CacheEntry> GetEnumerator()
=> _entries.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
private class CacheEntry
{
public CacheEntry(UIElement container)
{
Container = container;
}
public UIElement Container { get; }
public int Index { get; set; }
public object? Item { get; set; }
public Rect Bounds { get; set; }
}
private class CacheEntryComparer : IComparer<CacheEntry>
{
public static CacheEntryComparer Instance { get; } = new CacheEntryComparer();
public int Compare(CacheEntry x, CacheEntry y) => x.Index.CompareTo(y.Index);
}
private enum CacheEntryKind
{
New,
Kept,
Recycled
}
internal event VisibleIndicesUpdatedEventCallback VisibleIndicesUpdated;
private readonly ContainersCache _cache = new ContainersCache();
private CalendarLayoutStrategy? _layoutStrategy;
private CalendarViewGeneratorHost? _host;
private Rect _effectiveViewport;
private Rect _lastLayoutedViewport = Rect.Empty;
private void base_Initialize()
{
ContainerManager = new ContainerManager(this);
VerticalAlignment = VerticalAlignment.Top;
HorizontalAlignment = HorizontalAlignment.Left;
EffectiveViewportChanged += OnEffectiveViewportChanged;
}
/// <inheritdoc />
public double? ViewportWidth { get; private set; }
/// <inheritdoc />
public double? ViewportHeight { get; private set; }
#region Private and internal API required by UWP code
internal int FirstVisibleIndexBase { get; private set; } = -1;
internal int LastVisibleIndexBase { get; private set; } = -1;
internal int FirstCacheIndexBase => _cache.FirstIndex;
internal int LastCacheIndexBase => _cache.LastIndex;
[NotImplemented]
internal PanelScrollingDirection PanningDirectionBase { get; } = PanelScrollingDirection.None;
internal ILayoutStrategy? LayoutStrategy => _layoutStrategy;
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);
internal void ScrollItemIntoView(int index, ScrollIntoViewAlignment alignment, double offset, bool forceSynchronous)
{
if (_layoutStrategy is null)
{
return;
}
_layoutStrategy.EstimateElementBounds(ElementType.ItemContainer, index, default, default, default, out var bounds);
if (Owner?.ScrollViewer is { } sv)
{
var newOffset = bounds.Y + offset;
var currentOffset = sv.VerticalOffset;
// When we navigate between decade/month/year views, the CalendarView_Partial_Interaction.FocusItem
// will set the date which will invoke this ScrollItemIntoView method,
// then it will request GetContainerFromIndex and tries to focus it.
// So here we prepare the _effectiveViewport (which will most probably be re-updated by the ChangeView below),
// and then force a base_Measure()
_effectiveViewport.Y += newOffset - currentOffset;
sv.ChangeView(
horizontalOffset: null,
verticalOffset: newOffset,
zoomFactor: null,
forceSynchronous);
// Makes sure the container of the requested date is materialized before the end of this method
base_MeasureOverride(_lastLayoutedViewport.Size);
}
}
private Size GetViewportSize()
=> _lastLayoutedViewport.Size.AtLeast(_defaultHardCodedSize).FiniteOrDefault(_defaultHardCodedSize);
internal Size GetDesiredViewportSize()
=> _layoutStrategy?.GetDesiredViewportSize() ?? default;
[NotImplemented]
internal void GetTargetIndexFromNavigationAction(
int focusedIndex,
ElementType elementType,
KeyNavigationAction action,
object o,
int i,
out uint newFocusedIndexUint,
out ElementType newFocusedType,
out bool actionValidForSourceIndex)
{
newFocusedIndexUint = (uint)focusedIndex;
newFocusedType = elementType;
actionValidForSourceIndex = true;
}
internal IItemContainerMapping GetItemContainerMapping()
=> _cache;
private void SetLayoutStrategyBase(CalendarLayoutStrategy spLayoutStrategy)
{
_layoutStrategy = spLayoutStrategy;
spLayoutStrategy.LayoutDataInfoProvider = this;
}
private void CacheFirstVisibleElementBeforeOrientationChange()
{
}
private void ProcessOrientationChange()
{
}
/// <inheritdoc />
int ILayoutDataInfoProvider.GetTotalItemCount()
=> ContainerManager.TotalItemsCount;
/// <inheritdoc />
int ILayoutDataInfoProvider.GetTotalGroupCount()
=> ContainerManager.TotalGroupCount;
#endregion
#region Panel / base class (i.e. ModernCollectionBasePanel) implementation (Measure/Arrange)
private Rect GetLayoutViewport(Size availableSize = default)
{
if (_host is null)
{
return default;
}
// Compute the effective viewport of the panel (i.e. the portion of the panel for which we have to generate items)
// By default, if the CalendarView is not stretch, it will render at its default hardcoded size.
// It will also never be smaller than this hardcoded size (content will clipped)
// Note: If the Calendar has a defined size (or min / max) we ignore it in Measure, and we wait for the Arrange to "force" us to apply it.
var calendar = _host.Owner;
var viewport = new Rect(
_effectiveViewport.Location.FiniteOrDefault(default),
_effectiveViewport.Size.AtLeast(availableSize).AtLeast(_defaultHardCodedSize).FiniteOrDefault(_defaultHardCodedSize));
if (calendar.HorizontalAlignment != HorizontalAlignment.Stretch)
{
viewport.Width = _defaultHardCodedSize.Width;
}
if (calendar.VerticalAlignment != VerticalAlignment.Stretch)
{
viewport.Height = _defaultHardCodedSize.Height;
}
return viewport;
}
private Size base_MeasureOverride(Size availableSize)
{
if (_host is null || _layoutStrategy is null)
{
return default;
}
var viewport = GetLayoutViewport(availableSize);
_lastLayoutedViewport = viewport;
// Uno: This SetViewportSize should be done in the CalendarPanel_Partial.ArrangeOverride of the Panel(not the 'base_'),
// but (due to invalid layouting event sequence in uno?) it would cause a second layout pass.
// Invoking it here makes sure that Rows, Cols and ItemSize are valid for this measure pass.
ForceConfigViewport(viewport.Size);
_layoutStrategy.BeginMeasure();
#if __ANDROID__
using var a = PreventRequestLayout();
#else
ShouldInterceptInvalidate = true;
#endif
int index = -1, startIndex = 0, firstVisibleIndex = -1, lastVisibleIndex = -1;
var bottom = 0.0;
try
{
// We request to the algo to render 'preloadRows' extra rows before and after the actual viewport
var requestedRenderingWindow = viewport;
if (Rows > 0) // This can occur on first measure when we only determine the biggest item size
{
var pixelsPerRow = viewport.Height / Rows;
const double preloadRows = 1.5;
requestedRenderingWindow.Y = Math.Max(0, requestedRenderingWindow.Y - (preloadRows * pixelsPerRow));
requestedRenderingWindow.Height = requestedRenderingWindow.Height + (2 * preloadRows * pixelsPerRow);
}
// 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 startIndex);
renderWindow.Size = requestedRenderingWindow.Size; // The renderWindow contains only position information
startIndex = Math.Max(0, startIndex - StartIndex);
// Prepare the items generator to generate some new items (will also set which items can be recycled in this measure pass).
var expectedItemsCount = LastVisibleIndex - FirstVisibleIndex;
_cache.BeginGeneration(startIndex, startIndex + expectedItemsCount);
index = startIndex;
var count = _host.Count;
var layout = new LayoutReference { RelativeLocation = ReferenceIdentity.Myself };
var currentLine = (y: double.MinValue, col: 0);
while (
index < count
&&
// _layoutStrategy.ShouldContinueFillingUpSpace behaves weirdly, so we prefer to just check the bounds of the last measured element
// First we continue until we reach the last line, then we make sure to complete those line.
(layout.ReferenceBounds.Bottom < renderWindow.Bottom || currentLine.col < Cols)
)
{
var (entry, kind) = _cache.GetOrCreate(index);
if (kind == CacheEntryKind.New)
{
Children.Add(entry.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 + StartIndex, itemSize, layout, renderWindow);
bottom = itemBounds.Bottom;
if ((itemSize.Width < _minCellSize.Width && itemSize.Height < _minCellSize.Height) || Cols == 0 || Rows == 0)
{
// 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 _defaultHardCodedSize;
}
entry.Bounds = itemBounds;
entry.Container.Measure(itemSize);
entry.Container.GetVirtualizationInformation().MeasureSize = itemSize;
switch (kind)
{
case CacheEntryKind.New:
_host.SetupContainerContentChangingAfterPrepare(entry.Container, entry.Item, entry.Index, itemSize);
break;
case CacheEntryKind.Recycled:
// Note: ModernBasePanel seems to use only SetupContainerContentChangingAfterPrepare
_host.RaiseContainerContentChangingOnRecycle(entry.Container, entry.Item);
break;
}
var isVisible = itemBounds.IsIntersecting(viewport);
if (firstVisibleIndex == -1 && isVisible)
{
firstVisibleIndex = index;
lastVisibleIndex = index;
}
else if (isVisible)
{
lastVisibleIndex = index;
}
layout.RelativeLocation = ReferenceIdentity.AfterMe;
layout.ReferenceBounds = itemBounds;
if (currentLine.y < itemBounds.Y)
{
currentLine = (itemBounds.Y, 1);
}
else
{
currentLine.col++;
}
index++;
}
}
finally
{
try
{
FirstVisibleIndexBase = Math.Max(firstVisibleIndex, startIndex);
LastVisibleIndexBase = Math.Max(FirstVisibleIndexBase, lastVisibleIndex);
foreach (var unusedEntry in _cache.CompleteGeneration(index - 1))
{
Children.Remove(unusedEntry.Container);
}
global::System.Diagnostics.Debug.Assert(_cache.FirstIndex <= FirstVisibleIndex || FirstVisibleIndex == -1);
global::System.Diagnostics.Debug.Assert(_cache.LastIndex >= LastVisibleIndex || LastVisibleIndex == -1);
global::System.Diagnostics.Debug.Assert(Children.Count == _cache.LastIndex - _cache.FirstIndex + 1 || (_cache.LastIndex == -1 && _cache.LastIndex == -1));
}
catch
{
_cache.Clear();
Children.Clear();
InvalidateMeasure();
}
// We force the parent ScrollViewer to use the same viewport as us, no matter its own stretching.
ViewportHeight = viewport.Height;
#if !__ANDROID__
ShouldInterceptInvalidate = false;
#endif
_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);
// When the StartIndex is greater than 0, the desiredSize might ignore the last line.
if (desiredSize.Height < bottom)
{
desiredSize.Height = bottom;
}
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);
global::System.Diagnostics.Debug.Assert(Children.Count == _cache.LastIndex - _cache.FirstIndex + 1 || (_cache.LastIndex == -1 && _cache.LastIndex == -1));
var children = 0;
foreach (var entry in _cache)
{
children++;
var bounds = _layoutStrategy.GetElementArrangeBounds(ElementType.ItemContainer, entry.Index + StartIndex, entry.Bounds, window, finalSize);
entry.Container.Arrange(bounds);
entry.Container.GetVirtualizationInformation().Bounds = bounds;
}
if (children != Children.Count)
{
this.Log().Error("Invalid count of children ... fall-backing to slow arrange algorithm!");
foreach (var child in Children)
{
var index = _cache.IndexFromContainer(child);
var bounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index + StartIndex, child.DesiredSize, layout, window);
child.Arrange(bounds);
child.GetVirtualizationInformation().Bounds = bounds;
}
}
return finalSize;
}
#endregion
private static void OnEffectiveViewportChanged(FrameworkElement sender, EffectiveViewportChangedEventArgs args)
=> (sender as CalendarPanel)?.OnEffectiveViewportChanged(args);
private void OnEffectiveViewportChanged(EffectiveViewportChangedEventArgs args)
{
_effectiveViewport = args.EffectiveViewport;
if (_host is null || _layoutStrategy is null)
{
return;
}
var needsMeasure = ForceConfigViewport(GetLayoutViewport().Size);
if (needsMeasure || Math.Abs(_effectiveViewport.Y - _lastLayoutedViewport.Y) > (_lastLayoutedViewport.Height / Rows) * .75)
{
InvalidateMeasure();
}
}
private bool ForceConfigViewport(Size viewportSize)
{
// Uno: Those SetViewportSize and SetPanelDimension should be done in the CalendarPanel_Partial.ArrangeOverride of the Panel (not the 'base_'),
// but (due to invalid layouting event sequence in uno?) it would cause a second layout pass.
// Also on Android in Year and Decade views, the Arrange would never be invoked if the CellSize is not defined (0,0) ...
// which is actually set **ONLY** by this SetViewport for Year and Decade host
// (We bypass the SetItemMinimumSize in the CalendarPanel_Partial.MeasureOverride if m_type is **not** CalendarPanelType.Primary)
if (m_type == CalendarPanelType.Secondary_SelfAdaptive && m_biggestItemSize.Width > 2 && m_biggestItemSize.Height > 2)
{
int effectiveCols = (int)(viewportSize.Width / m_biggestItemSize.Width);
int effectiveRows = (int)(viewportSize.Height / m_biggestItemSize.Height);
effectiveCols = Math.Max(1, Math.Min(effectiveCols, m_suggestedCols));
effectiveRows = Math.Max(1, Math.Min(effectiveRows, m_suggestedRows));
SetPanelDimension(effectiveCols, effectiveRows);
}
if (Rows == 0 || Cols == 0)
{
return false;
}
_layoutStrategy!.SetViewportSize(viewportSize, out var needsMeasure);
return needsMeasure;
}
}
internal class ContainerManager
{
// Required properties from WinUI code
public int StartOfContainerVisualSection() => Math.Max(0, _owner.FirstVisibleIndex);
public int TotalItemsCount => Host?.Count ?? 0;
public int TotalGroupCount = 0;
// Uno only
private readonly CalendarPanel _owner;
public CalendarViewGeneratorHost? Host { get; set; }
public ContainerManager(CalendarPanel owner)
{
_owner = owner;
}
}
}