Skip to content

Instantly share code, notes, and snippets.

@VincentH-Net
Last active February 16, 2026 13:40
Show Gist options
  • Select an option

  • Save VincentH-Net/3ec3a5f89acc85707c434ade4466dfa6 to your computer and use it in GitHub Desktop.

Select an option

Save VincentH-Net/3ec3a5f89acc85707c434ade4466dfa6 to your computer and use it in GitHub Desktop.
A responsive Uno Platform / WinUI layout that arranges items of the same height and varying widths in a wrapping grid. Supports column spans, proportional stretch-to-fill per row, and vertical alignment of horizontal gaps across rows.. Use for e.g. dashboards.
using Windows.Foundation;
namespace MyApp.Presentation.Controls; // TODO: Replace 'MyApp' with your app's top-level namespace
/// <summary>
/// A responsive, non-virtualizing layout that arranges items of the same height and varying widths in a wrapping grid.<br/>
/// Supports column spans, proportional stretch-to-fill per row, and vertical alignment of horizontal gaps across rows. Use for e.g. dashboards.<br/>
/// Do NOT enable horizontal scrolling for this layout, as this will break the layout's ability to measure available width and proportionally stretch items to fill horizontal space.
/// </summary>
/// <remarks>
/// The layout meets the following requirements:<br/>
/// 1. Flows items left to right, top to bottom<br/>
/// 2. Respects the columnspan of each item (all items have the same height). This means that no row can grow shorter than the widest columnspan in the collection<br/>
/// 3. Prevents any remaining horizontal space at the end of each row by increasing the width of the row's items proportionally<br/>
/// 4. Ensures that any horizontal gaps between items align vertically across rows. If this causes a gap at the end of a row, the last item in the row grows wider to eliminate the gap.<br/>
///
/// Source: https://gist.github.com/VincentH-Net/3ec3a5f89acc85707c434ade4466dfa6
/// </remarks>
public class ResponsiveSpanningGridWrapLayout : NonVirtualizingLayout
{
#region Dependency Properties
public static readonly DependencyProperty MinColumnWidthProperty =
DependencyProperty.Register(
nameof(MinColumnWidth), typeof(double), typeof(ResponsiveSpanningGridWrapLayout),
new PropertyMetadata(200.0, OnLayoutPropertyChanged));
public static readonly DependencyProperty ColumnSpacingProperty =
DependencyProperty.Register(
nameof(ColumnSpacing), typeof(double), typeof(ResponsiveSpanningGridWrapLayout),
new PropertyMetadata(8.0, OnLayoutPropertyChanged));
public static readonly DependencyProperty RowSpacingProperty =
DependencyProperty.Register(
nameof(RowSpacing), typeof(double), typeof(ResponsiveSpanningGridWrapLayout),
new PropertyMetadata(8.0, OnLayoutPropertyChanged));
public static readonly DependencyProperty ItemHeightProperty =
DependencyProperty.Register(
nameof(ItemHeight), typeof(double), typeof(ResponsiveSpanningGridWrapLayout),
new PropertyMetadata(200.0, OnLayoutPropertyChanged));
public double MinColumnWidth
{
get => (double)GetValue(MinColumnWidthProperty);
set => SetValue(MinColumnWidthProperty, value);
}
public double ColumnSpacing
{
get => (double)GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
public double RowSpacing
{
get => (double)GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
public double ItemHeight
{
get => (double)GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
#endregion
#region Attached Property: ColumnSpan
public static readonly DependencyProperty ColumnSpanProperty =
DependencyProperty.RegisterAttached(
"ColumnSpan", typeof(int), typeof(ResponsiveSpanningGridWrapLayout),
new PropertyMetadata(1));
public static void SetColumnSpan(DependencyObject element, int value)
{
ArgumentNullException.ThrowIfNull(element);
element.SetValue(ColumnSpanProperty, value);
}
public static int GetColumnSpan(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (int)element.GetValue(ColumnSpanProperty);
}
#endregion
static void OnLayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ResponsiveSpanningGridWrapLayout layout)
{
layout.InvalidateMeasure();
}
}
record struct LayoutSlot(int Index, double X, double Y, double Width, double Height);
List<LayoutSlot> ComputeLayout(NonVirtualizingLayoutContext context, double availableWidth)
{
List<LayoutSlot> slots = [];
int childCount = context.Children.Count;
if (childCount == 0 || availableWidth <= 0)
{
return slots;
}
// Fallback for infinite width
if (double.IsInfinity(availableWidth))
{
availableWidth = 800;
}
// Ensure available width can fit the widest item at its minimum span
int maxRequestedSpan = 1;
for (int i = 0; i < childCount; i++)
{
maxRequestedSpan = Math.Max(maxRequestedSpan, Math.Max(GetColumnSpan(context.Children[i]), 1));
}
double minLayoutWidth = MinColumnWidth * maxRequestedSpan + (maxRequestedSpan - 1) * ColumnSpacing;
availableWidth = Math.Max(availableWidth, minLayoutWidth);
// Compute maxColumns with the enforced minimum width
int maxColumns = Math.Max(1, (int)Math.Floor((availableWidth + ColumnSpacing) / (MinColumnWidth + ColumnSpacing)));
// Build rows using greedy packing
List<List<(int index, int span)>> rows = [];
List<(int index, int span)> currentRow = [];
int currentRowSpan = 0;
for (int i = 0; i < childCount; i++)
{
int requestedSpan = Math.Max(GetColumnSpan(context.Children[i]), 1);
int span = Math.Min(requestedSpan, maxColumns);
if (currentRowSpan + span > maxColumns && currentRow.Count > 0)
{
rows.Add(currentRow);
currentRow = [];
currentRowSpan = 0;
}
currentRow.Add((i, span));
currentRowSpan += span;
}
if (currentRow.Count > 0)
{
rows.Add(currentRow);
}
// Compute a consistent unit width based on the full column grid
double gridSpacing = (maxColumns - 1) * ColumnSpacing;
double unitWidth = (availableWidth - gridSpacing) / maxColumns;
// Compute slots per row, stretching last item to fill any trailing gap
double y = 0.0;
foreach (var row in rows)
{
double x = 0.0;
for (int r = 0; r < row.Count; r++)
{
(int index, int span) = row[r];
double itemWidth = unitWidth * span + (span - 1) * ColumnSpacing;
if (r == row.Count - 1)
{
itemWidth = availableWidth - x;
}
slots.Add(new LayoutSlot(index, x, y, itemWidth, ItemHeight));
x += itemWidth + ColumnSpacing;
}
y += ItemHeight + RowSpacing;
}
return slots;
}
protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize)
{
ArgumentNullException.ThrowIfNull(context);
var slots = ComputeLayout(context, availableSize.Width);
if (slots.Count == 0)
{
return new Size(0, 0);
}
// Measure each child with its computed size
foreach (var slot in slots)
{
context.Children[slot.Index].Measure(new Size(slot.Width, slot.Height));
}
double totalHeight = slots.Max(s => s.Y + s.Height);
double neededWidth = slots.Max(s => s.X + s.Width);
double width = double.IsInfinity(availableSize.Width) ? neededWidth : Math.Max(neededWidth, availableSize.Width);
return new Size(width, totalHeight);
}
protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
{
ArgumentNullException.ThrowIfNull(context);
var slots = ComputeLayout(context, finalSize.Width);
if (slots.Count == 0)
{
return new Size(0, 0);
}
foreach (var slot in slots)
{
context.Children[slot.Index].Arrange(new Rect(slot.X, slot.Y, slot.Width, slot.Height));
}
double totalHeight = slots.Max(s => s.Y + s.Height);
return new Size(finalSize.Width, totalHeight);
}
}
@VincentH-Net
Copy link
Author

See video👇

Note how:

  1. The vertical dividers always align
  2. The card widths grow / shrink to always fill all available width - proportionally
  3. Only the last card in a row sometimes grows more than proportional, if 1. requires that
ResponsiveSpanningGridWrapLayout.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment