Browse Source

fix(grid): allow shared size groups to shrink (#21837)

* test(grid): reproduce shared size group shrink failure

Shared auto columns expand when grouped content becomes visible but keep the expanded width after that content is hidden. Cover the full zero, expanded, and shrunk layout sequence so the stale shared minimum cannot regress unnoticed.

* fix(grid): allow shared size groups to shrink

Shared auto definitions reused the previous group minimum while recomputing the next group minimum, so an expanded width became self-sustaining after content shrank. Aggregate each definition's intrinsic measured minimum instead, preserving the shared-size invariant and matching the canonical WPF algorithm.

* test(grid): reproduce shared size group stuck on cyclic measure path

A grid mixing an auto-column/star-row cell with a star-column/auto-row
cell measures through Grid's cyclic dependency path, which saves and
restores definition min sizes around the repeated measure. The saved
value is the effective min size, so the group minimum is folded into the
definition's own contribution and the group can never shrink below it.

* fix(grid): restore raw min sizes on the cyclic measure path

Grid.CacheMinSizes read DefinitionBase.MinSize, which already includes
the shared group's minimum, but ApplyCachedMinSizes writes it back
through SetMinSize, which assigns the definition's own _minSize. A
short-pole definition therefore adopted the group minimum as its own
intrinsic minimum, which reclassified it as a long pole on the next
validation. Long poles with a valid measure are never remeasured, so the
fabricated minimum became unreachable and pinned the group open.

Add DefinitionBase.RawMinSize, the read counterpart of SetMinSize, and
cache that instead so the save/restore pair round-trips the definition's
own contribution. Matches the WPF fix in dotnet/wpf#2270.
pull/21857/head
Nathan Nguyen 2 months ago
committed by GitHub
parent
commit
0ab8b2fe58
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      src/Avalonia.Controls/DefinitionBase.cs
  2. 4
      src/Avalonia.Controls/Grid.cs
  3. 97
      tests/Avalonia.Controls.UnitTests/GridTests.cs

13
src/Avalonia.Controls/DefinitionBase.cs

@ -269,6 +269,17 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Returns min size, never taking into account shared state.
/// </summary>
/// <remarks>
/// This is the definition's own intrinsic contribution to its group. It is the counterpart
/// of <see cref="SetMinSize"/>: code that saves and restores a min size across a measure
/// must round-trip this value, because feeding the group minimum back into
/// <see cref="_minSize"/> would make the group unable to shrink.
/// </remarks>
internal double RawMinSize => _minSize;
/// <summary>
/// Offset.
/// </summary>
@ -626,7 +637,7 @@ namespace Avalonia.Controls
// accumulate min size of all participating definitions
for (int i = 0, count = _registry.Count; i < count; ++i)
{
sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize);
sharedMinSize = Math.Max(sharedMinSize, _registry[i]._minSize);
}
bool sharedMinSizeChanged = !MathUtilities.AreClose(_minSize, sharedMinSize);

4
src/Avalonia.Controls/Grid.cs

@ -946,11 +946,11 @@ namespace Avalonia.Controls
{
if (isRows)
{
minSizes[PrivateCells[i].RowIndex] = DefinitionsV[PrivateCells[i].RowIndex].MinSize;
minSizes[PrivateCells[i].RowIndex] = DefinitionsV[PrivateCells[i].RowIndex].RawMinSize;
}
else
{
minSizes[PrivateCells[i].ColumnIndex] = DefinitionsU[PrivateCells[i].ColumnIndex].MinSize;
minSizes[PrivateCells[i].ColumnIndex] = DefinitionsU[PrivateCells[i].ColumnIndex].RawMinSize;
}
i = PrivateCells[i].Next;

97
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1212,6 +1212,103 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(10, grids[1].ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Shared_Size_Group_Shrinks_When_Content_Is_Hidden()
{
var grids = new[]
{
CreateGrid(("A", GridLength.Auto)),
CreateGrid(("A", GridLength.Auto)),
};
var content = new Border
{
Width = 50,
IsVisible = false,
};
grids[1].Children.Add(content);
var scope = new StackPanel
{
[Grid.IsSharedSizeScopeProperty] = true,
Children =
{
grids[0],
grids[1],
},
};
var root = new TestRoot(scope);
void ExecuteSharedSizeLayoutPass()
{
// Shared groups validate after layout and apply any resulting invalidation on the next pass.
root.LayoutManager.ExecuteLayoutPass();
root.LayoutManager.ExecuteLayoutPass();
}
root.ExecuteInitialLayoutPass();
Assert.All(grids, grid => Assert.Equal(0, grid.ColumnDefinitions[0].ActualWidth));
content.IsVisible = true;
ExecuteSharedSizeLayoutPass();
Assert.All(grids, grid => Assert.Equal(50, grid.ColumnDefinitions[0].ActualWidth));
content.IsVisible = false;
ExecuteSharedSizeLayoutPass();
Assert.All(grids, grid => Assert.Equal(0, grid.ColumnDefinitions[0].ActualWidth));
}
[Fact]
public void Shared_Size_Group_Shrinks_When_Participant_Uses_Cyclic_Measure_Path()
{
// A grid mixing an auto-column/star-row cell with a star-column/auto-row cell cannot
// resolve stars in either direction up front, so Grid measures it through its cyclic
// dependency path. That path saves definition min sizes before the repeated measure and
// restores them afterwards. Saving the effective min size folds the group minimum into
// the definition's own contribution, which then reclassifies it as a long pole - and
// long poles are deliberately never remeasured, so the group stays pinned open.
var cyclicGrid = CreateGrid(("A", GridLength.Auto), (null, new GridLength(1, GridUnitType.Star)));
cyclicGrid.Height = 100; // star rows collapse to auto under an infinite constraint.
cyclicGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
cyclicGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
var autoColumnStarRowChild = new Border { Width = 10, Height = 10 };
Grid.SetColumn(autoColumnStarRowChild, 0);
Grid.SetRow(autoColumnStarRowChild, 1);
var starColumnAutoRowChild = new Border { Width = 10, Height = 10 };
Grid.SetColumn(starColumnAutoRowChild, 1);
Grid.SetRow(starColumnAutoRowChild, 0);
cyclicGrid.Children.Add(autoColumnStarRowChild);
cyclicGrid.Children.Add(starColumnAutoRowChild);
var plainGrid = CreateGrid(("A", GridLength.Auto), (null, new GridLength(1, GridUnitType.Star)));
var wideContent = new Border { Width = 50, Height = 10 };
plainGrid.Children.Add(wideContent);
var scope = new StackPanel
{
[Grid.IsSharedSizeScopeProperty] = true,
Children = { cyclicGrid, plainGrid },
};
var root = new TestRoot(scope);
void ExecuteSharedSizeLayoutPass()
{
// Shared groups validate after layout and apply any resulting invalidation on the next pass.
root.LayoutManager.ExecuteLayoutPass();
root.LayoutManager.ExecuteLayoutPass();
}
root.ExecuteInitialLayoutPass();
ExecuteSharedSizeLayoutPass();
Assert.Equal(50, cyclicGrid.ColumnDefinitions[0].ActualWidth);
Assert.Equal(50, plainGrid.ColumnDefinitions[0].ActualWidth);
wideContent.IsVisible = false;
ExecuteSharedSizeLayoutPass();
Assert.Equal(10, cyclicGrid.ColumnDefinitions[0].ActualWidth);
Assert.Equal(10, plainGrid.ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Collection_Changes_Are_Tracked()
{

Loading…
Cancel
Save