Browse Source

Merge remote-tracking branch 'upstream/master' into fixes/getTextBounds

pull/11066/head
Benedikt Stebner 3 years ago
parent
commit
2b9f32dfd3
  1. 2
      src/Avalonia.Base/Layout/LayoutManager.cs
  2. 18
      src/Avalonia.Base/Layout/LayoutQueue.cs
  3. 40
      src/Avalonia.Base/Layout/Layoutable.cs
  4. 15
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  5. 28
      src/Avalonia.Controls/Utils/RealizedStackElements.cs
  6. 34
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  7. 23
      tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
  8. 35
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

2
src/Avalonia.Base/Layout/LayoutManager.cs

@ -17,7 +17,7 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager, IDisposable
{
private const int MaxPasses = 3;
private const int MaxPasses = 10;
private readonly Layoutable _owner;
private readonly LayoutQueue<Layoutable> _toMeasure = new LayoutQueue<Layoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<Layoutable> _toArrange = new LayoutQueue<Layoutable>(v => !v.IsArrangeValid);

18
src/Avalonia.Base/Layout/LayoutQueue.cs

@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Logging;
namespace Avalonia.Layout
{
@ -48,10 +49,21 @@ namespace Avalonia.Layout
{
_loopQueueInfo.TryGetValue(item, out var info);
if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
if (!info.Active)
{
_inner.Enqueue(item);
_loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
if (info.Count < _maxEnqueueCountPerLoop)
{
_inner.Enqueue(item);
_loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
}
else
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log(
this,
"Layout cycle detected. Item {Item} was enqueued {Count} times.",
item,
info.Count);
}
}
}

40
src/Avalonia.Base/Layout/Layoutable.cs

@ -776,10 +776,24 @@ namespace Avalonia.Layout
// All changes to visibility cause the parent element to be notified.
this.GetVisualParent<Layoutable>()?.ChildDesiredSizeChanged(this);
// We only invalidate outselves when visibility is changed to true.
if (change.GetNewValue<bool>())
{
// We only invalidate ourselves when visibility is changed to true.
InvalidateMeasure();
// If any descendant had its measure/arrange invalidated while we were hidden,
// they will need to to be registered with the layout manager now that they
// are again effectively visible. If IsEffectivelyVisible becomes an observable
// property then we can piggy-pack on that; for the moment we do this manually.
if (VisualRoot is ILayoutRoot layoutRoot)
{
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
(VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager);
}
}
}
}
}
@ -804,6 +818,30 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
private void AncestorBecameVisible(ILayoutManager layoutManager)
{
if (!IsVisible)
return;
if (!IsMeasureValid)
{
layoutManager.InvalidateMeasure(this);
InvalidateVisual();
}
else if (!IsArrangeValid)
{
layoutManager.InvalidateArrange(this);
InvalidateVisual();
}
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
(VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager);
}
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>

15
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -85,7 +85,7 @@ namespace Avalonia.Controls.Generators
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// only needs to be called a single time, otherwise this method should be called after the
/// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// </remarks>
@ -100,10 +100,11 @@ namespace Avalonia.Controls.Generators
/// <param name="item">The item being displayed.</param>
/// <param name="index">The index of the item being displayed.</param>
/// <remarks>
/// This method should be called when a container has been fully prepared and added
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It should be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/>.
/// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
/// must be called only a single time.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -122,6 +123,12 @@ namespace Avalonia.Controls.Generators
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
/// </summary>
/// <param name="container">The container control.</param>
/// <remarks>
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
[Obsolete("Use ItemsControl.ContainerFromIndex")]

28
src/Avalonia.Controls/Utils/RealizedStackElements.cs

@ -353,7 +353,10 @@ namespace Avalonia.Controls.Utils
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
{
_elements[i] = null;
recycleElement(element);
}
}
_elements.RemoveRange(start, end - start);
@ -389,10 +392,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
foreach (var e in _elements)
for (var i = 0; i < _elements.Count; i++)
{
if (e is not null)
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e);
}
}
_startU = _firstIndex = 0;
@ -422,7 +428,10 @@ namespace Avalonia.Controls.Utils
for (var i = 0; i < endIndex; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(0, endIndex);
@ -453,7 +462,10 @@ namespace Avalonia.Controls.Utils
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(startIndex, _elements.Count - startIndex);
@ -470,13 +482,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
var i = FirstIndex;
foreach (var e in _elements)
for (var i = 0; i < _elements.Count; i++)
{
if (e is not null)
recycleElement(e, i);
++i;
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_startU = _firstIndex = 0;

34
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -565,7 +565,6 @@ namespace Avalonia.Controls
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
InvalidateHack(e);
return e;
}
@ -713,39 +712,6 @@ namespace Avalonia.Controls
}
}
private static void InvalidateHack(Control c)
{
bool HasInvalidations(Control c)
{
if (!c.IsMeasureValid)
return true;
for (var i = 0; i < c.VisualChildren.Count; ++i)
{
if (c.VisualChildren[i] is Control child)
{
if (!child.IsMeasureValid || HasInvalidations(child))
return true;
}
}
return false;
}
void Invalidate(Control c)
{
c.InvalidateMeasure();
for (var i = 0; i < c.VisualChildren.Count; ++i)
{
if (c.VisualChildren[i] is Control child)
Invalidate(child);
}
}
if (HasInvalidations(c))
Invalidate(c);
}
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
{
if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)

23
tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs

@ -59,6 +59,29 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.False(control.Arranged);
}
[Fact]
public void Lays_Out_Descendents_That_Were_Invalidated_While_Ancestor_Was_Not_Visible()
{
// Issue #11076
var control = new LayoutTestControl();
var parent = new Decorator { Child = control };
var grandparent = new Decorator { Child = parent };
var root = new LayoutTestRoot { Child = grandparent };
root.LayoutManager.ExecuteInitialLayoutPass();
grandparent.IsVisible = false;
control.InvalidateMeasure();
root.LayoutManager.ExecuteInitialLayoutPass();
grandparent.IsVisible = true;
root.LayoutManager.ExecuteLayoutPass();
Assert.True(control.IsMeasureValid);
Assert.True(control.IsArrangeValid);
}
[Fact]
public void Arranges_InvalidateArranged_Control()
{

35
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1074,6 +1074,40 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems);
}
[Fact]
public void Selection_Is_Not_Cleared_On_Recycling_Containers()
{
using var app = Start();
var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList();
// Create a SelectingItemsControl that creates containers that raise IsSelectedChanged,
// with a virtualizing stack panel.
var target = CreateTarget<TestSelectorWithContainers>(
itemsSource: items,
virtualizing: true);
target.AutoScrollToSelectedItem = false;
var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
var scroll = panel.FindAncestorOfType<ScrollViewer>()!;
// Select item 1.
target.SelectedIndex = 1;
// Scroll item 1 out of view.
scroll.Offset = new(0, 1000);
Layout(target);
Assert.Equal(10, panel.FirstRealizedIndex);
Assert.Equal(19, panel.LastRealizedIndex);
// The selection should be preserved.
Assert.Empty(SelectedContainers(target));
Assert.Equal(1, target.SelectedIndex);
Assert.Same(items[1], target.SelectedItem);
Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);
Assert.Equal(new[] { items[1] }, target.Selection.SelectedItems);
}
[Fact]
public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
{
@ -1248,6 +1282,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Setters =
{
new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()),
new Setter(TestContainer.HeightProperty, 100.0),
},
};
}

Loading…
Cancel
Save