// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform.Linux.Services; using SkiaSharp; using Microsoft.Maui; namespace Microsoft.Maui.Platform; /// /// Grid layout that arranges children in rows and columns. /// public class SkiaGrid : SkiaLayoutView { #region BindableProperties /// /// Bindable property for RowSpacing. /// public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create( nameof(RowSpacing), typeof(float), typeof(SkiaGrid), 0f, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure()); /// /// Bindable property for ColumnSpacing. /// public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create( nameof(ColumnSpacing), typeof(float), typeof(SkiaGrid), 0f, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure()); #endregion private readonly List _rowDefinitions = new(); private readonly List _columnDefinitions = new(); private readonly Dictionary _childPositions = new(); private float[] _rowHeights = Array.Empty(); private float[] _columnWidths = Array.Empty(); /// /// Gets the row definitions. /// public IList RowDefinitions => _rowDefinitions; /// /// Gets the column definitions. /// public IList ColumnDefinitions => _columnDefinitions; /// /// Spacing between rows. /// public float RowSpacing { get => (float)GetValue(RowSpacingProperty); set => SetValue(RowSpacingProperty, value); } /// /// Spacing between columns. /// public float ColumnSpacing { get => (float)GetValue(ColumnSpacingProperty); set => SetValue(ColumnSpacingProperty, value); } /// /// Adds a child at the specified grid position. /// public void AddChild(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) { base.AddChild(child); _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); } public override void RemoveChild(SkiaView child) { base.RemoveChild(child); _childPositions.Remove(child); } /// /// Gets the grid position of a child. /// public GridPosition GetPosition(SkiaView child) { return _childPositions.TryGetValue(child, out var pos) ? pos : new GridPosition(0, 0, 1, 1); } /// /// Sets the grid position of a child. /// public void SetPosition(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) { _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); InvalidateMeasure(); Invalidate(); } protected override Size MeasureOverride(Size availableSize) { var contentWidth = (float)(availableSize.Width - Padding.Left - Padding.Right); var contentHeight = (float)(availableSize.Height - Padding.Top - Padding.Bottom); // Handle NaN/Infinity if (float.IsNaN(contentWidth) || float.IsInfinity(contentWidth)) contentWidth = 800; if (float.IsNaN(contentHeight) || float.IsInfinity(contentHeight)) contentHeight = float.PositiveInfinity; var rowCount = Math.Max(1, _rowDefinitions.Count > 0 ? _rowDefinitions.Count : GetMaxRow() + 1); var columnCount = Math.Max(1, _columnDefinitions.Count > 0 ? _columnDefinitions.Count : GetMaxColumn() + 1); // First pass: measure children in Auto columns to get natural widths var columnNaturalWidths = new float[columnCount]; var rowNaturalHeights = new float[rowCount]; foreach (var child in Children) { if (!child.IsVisible) continue; var pos = GetPosition(child); // For Auto columns, measure with infinite width to get natural size var def = pos.Column < _columnDefinitions.Count ? _columnDefinitions[pos.Column] : GridLength.Star; if (def.IsAuto && pos.ColumnSpan == 1) { var childSize = child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); var childWidth = double.IsNaN(childSize.Width) ? 0f : (float)childSize.Width; columnNaturalWidths[pos.Column] = Math.Max(columnNaturalWidths[pos.Column], childWidth); } } // Calculate column widths - handle Auto, Absolute, and Star _columnWidths = CalculateSizesWithAuto(_columnDefinitions, contentWidth, ColumnSpacing, columnCount, columnNaturalWidths); // Second pass: measure all children with calculated column widths foreach (var child in Children) { if (!child.IsVisible) continue; var pos = GetPosition(child); var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan); // Give infinite height for initial measure var childSize = child.Measure(new Size(cellWidth, double.PositiveInfinity)); // Track max height for each row // Cap infinite/very large heights - child returning infinity means it doesn't have a natural height var childHeight = (float)childSize.Height; if (float.IsNaN(childHeight) || float.IsInfinity(childHeight) || childHeight > 100000) { // Use a default minimum - will be expanded by Star sizing if finite height is available childHeight = 44; // Standard row height } if (pos.RowSpan == 1) { rowNaturalHeights[pos.Row] = Math.Max(rowNaturalHeights[pos.Row], childHeight); } } // Calculate row heights - use natural heights when available height is infinite or very large // (Some layouts pass float.MaxValue instead of PositiveInfinity) if (float.IsInfinity(contentHeight) || contentHeight > 100000) { _rowHeights = rowNaturalHeights; } else { _rowHeights = CalculateSizesWithAuto(_rowDefinitions, contentHeight, RowSpacing, rowCount, rowNaturalHeights); } // Third pass: re-measure children with actual cell sizes foreach (var child in Children) { if (!child.IsVisible) continue; var pos = GetPosition(child); var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan); var cellHeight = GetCellHeight(pos.Row, pos.RowSpan); child.Measure(new Size(cellWidth, cellHeight)); } // Calculate total size var totalWidth = _columnWidths.Sum() + Math.Max(0, columnCount - 1) * ColumnSpacing; var totalHeight = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing; return new Size( totalWidth + Padding.Left + Padding.Right, totalHeight + Padding.Top + Padding.Bottom); } private int GetMaxRow() { int maxRow = 0; foreach (var pos in _childPositions.Values) { maxRow = Math.Max(maxRow, pos.Row + pos.RowSpan - 1); } return maxRow; } private int GetMaxColumn() { int maxCol = 0; foreach (var pos in _childPositions.Values) { maxCol = Math.Max(maxCol, pos.Column + pos.ColumnSpan - 1); } return maxCol; } private float[] CalculateSizesWithAuto(List definitions, float available, float spacing, int count, float[] naturalSizes) { if (count == 0) return new float[] { available }; var sizes = new float[count]; var totalSpacing = Math.Max(0, count - 1) * spacing; var remainingSpace = available - totalSpacing; // First pass: absolute and auto sizes float starTotal = 0; for (int i = 0; i < count; i++) { var def = i < definitions.Count ? definitions[i] : GridLength.Star; if (def.IsAbsolute) { sizes[i] = def.Value; remainingSpace -= def.Value; } else if (def.IsAuto) { // Use natural size from measured children sizes[i] = naturalSizes[i]; remainingSpace -= sizes[i]; } else if (def.IsStar) { starTotal += def.Value; } } // Second pass: star sizes (distribute remaining space) if (starTotal > 0 && remainingSpace > 0) { for (int i = 0; i < count; i++) { var def = i < definitions.Count ? definitions[i] : GridLength.Star; if (def.IsStar) { sizes[i] = (def.Value / starTotal) * remainingSpace; } } } return sizes; } private float GetCellWidth(int column, int span) { float width = 0; for (int i = column; i < Math.Min(column + span, _columnWidths.Length); i++) { width += _columnWidths[i]; if (i > column) width += ColumnSpacing; } return width; } private float GetCellHeight(int row, int span) { float height = 0; for (int i = row; i < Math.Min(row + span, _rowHeights.Length); i++) { height += _rowHeights[i]; if (i > row) height += RowSpacing; } return height; } private float GetColumnOffset(int column) { float offset = 0; for (int i = 0; i < Math.Min(column, _columnWidths.Length); i++) { offset += _columnWidths[i] + ColumnSpacing; } return offset; } private float GetRowOffset(int row) { float offset = 0; for (int i = 0; i < Math.Min(row, _rowHeights.Length); i++) { offset += _rowHeights[i] + RowSpacing; } return offset; } protected override Rect ArrangeOverride(Rect bounds) { try { var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom)); // Recalculate row heights for arrange bounds if they differ from measurement // This ensures Star rows expand to fill available space var rowCount = _rowHeights.Length > 0 ? _rowHeights.Length : 1; var columnCount = _columnWidths.Length > 0 ? _columnWidths.Length : 1; var arrangeRowHeights = _rowHeights; // If we have arrange height and rows need recalculating if (content.Height > 0 && !float.IsInfinity(content.Height)) { var measuredRowsTotal = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing; // If arrange height is larger than measured, redistribute to Star rows if (content.Height > measuredRowsTotal + 1) { arrangeRowHeights = new float[rowCount]; var extraHeight = content.Height - measuredRowsTotal; // Count Star rows (implicit rows without definitions are Star) float totalStarWeight = 0; for (int i = 0; i < rowCount; i++) { var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; if (def.IsStar) totalStarWeight += def.Value; } // Distribute extra height to Star rows for (int i = 0; i < rowCount; i++) { var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; arrangeRowHeights[i] = i < _rowHeights.Length ? _rowHeights[i] : 0; if (def.IsStar && totalStarWeight > 0) { arrangeRowHeights[i] += extraHeight * (def.Value / totalStarWeight); } } } else { arrangeRowHeights = _rowHeights; } } foreach (var child in Children) { if (!child.IsVisible) continue; var pos = GetPosition(child); var x = content.Left + GetColumnOffset(pos.Column); // Calculate y using arrange row heights float y = content.Top; for (int i = 0; i < Math.Min(pos.Row, arrangeRowHeights.Length); i++) { y += arrangeRowHeights[i] + RowSpacing; } var width = GetCellWidth(pos.Column, pos.ColumnSpan); // Calculate height using arrange row heights float height = 0; for (int i = pos.Row; i < Math.Min(pos.Row + pos.RowSpan, arrangeRowHeights.Length); i++) { height += arrangeRowHeights[i]; if (i > pos.Row) height += RowSpacing; } // Clamp infinite dimensions if (float.IsInfinity(width) || float.IsNaN(width)) width = content.Width; if (float.IsInfinity(height) || float.IsNaN(height) || height <= 0) height = content.Height; // Apply child's margin var margin = child.Margin; var cellX = x + (float)margin.Left; var cellY = y + (float)margin.Top; var cellWidth = width - (float)margin.Left - (float)margin.Right; var cellHeight = height - (float)margin.Top - (float)margin.Bottom; // Get child's desired size var childDesiredSize = child.Measure(new Size(cellWidth, cellHeight)); var childWidth = (float)childDesiredSize.Width; var childHeight = (float)childDesiredSize.Height; var vAlign = (int)child.VerticalOptions.Alignment; // Apply HorizontalOptions // LayoutAlignment: Start=0, Center=1, End=2, Fill=3 float finalX = cellX; float finalWidth = cellWidth; var hAlign = (int)child.HorizontalOptions.Alignment; if (hAlign != 3 && childWidth < cellWidth && childWidth > 0) // 3 = Fill { finalWidth = childWidth; if (hAlign == 1) // Center finalX = cellX + (cellWidth - childWidth) / 2; else if (hAlign == 2) // End finalX = cellX + cellWidth - childWidth; } // Apply VerticalOptions float finalY = cellY; float finalHeight = cellHeight; // vAlign already calculated above for debug logging if (vAlign != 3 && childHeight < cellHeight && childHeight > 0) // 3 = Fill { finalHeight = childHeight; if (vAlign == 1) // Center finalY = cellY + (cellHeight - childHeight) / 2; else if (vAlign == 2) // End finalY = cellY + cellHeight - childHeight; } child.Arrange(new Rect(finalX, finalY, finalWidth, finalHeight)); } return bounds; } catch (Exception ex) { DiagnosticLog.Error("SkiaGrid", $"EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}", ex); DiagnosticLog.Error("SkiaGrid", $"Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}"); DiagnosticLog.Error("SkiaGrid", $"Stack trace: {ex.StackTrace}"); throw; } } } /// /// Grid position information. /// public readonly struct GridPosition { public int Row { get; } public int Column { get; } public int RowSpan { get; } public int ColumnSpan { get; } public GridPosition(int row, int column, int rowSpan = 1, int columnSpan = 1) { Row = row; Column = column; RowSpan = Math.Max(1, rowSpan); ColumnSpan = Math.Max(1, columnSpan); } } /// /// Grid length specification. /// public readonly struct GridLength { public float Value { get; } public GridUnitType GridUnitType { get; } public bool IsAbsolute => GridUnitType == GridUnitType.Absolute; public bool IsAuto => GridUnitType == GridUnitType.Auto; public bool IsStar => GridUnitType == GridUnitType.Star; public static GridLength Auto => new(1, GridUnitType.Auto); public static GridLength Star => new(1, GridUnitType.Star); public GridLength(float value, GridUnitType unitType = GridUnitType.Absolute) { Value = value; GridUnitType = unitType; } public static GridLength FromAbsolute(float value) => new(value, GridUnitType.Absolute); public static GridLength FromStar(float value = 1) => new(value, GridUnitType.Star); } /// /// Grid unit type options. /// public enum GridUnitType { Absolute, Star, Auto }