Split LinuxApplication into Input and Lifecycle partials. Extract SkiaView into Accessibility, Drawing, and Input partials. Split SkiaEntry and SkiaEditor into Drawing and Input partials. Extract TextRenderingHelper from SkiaRenderingEngine. Create dedicated files for SkiaAbsoluteLayout, SkiaGrid, and SkiaStackLayout. This reduces file sizes from 40K+ lines to manageable units organized by responsibility.
501 lines
17 KiB
C#
501 lines
17 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Grid layout that arranges children in rows and columns.
|
|
/// </summary>
|
|
public class SkiaGrid : SkiaLayoutView
|
|
{
|
|
#region BindableProperties
|
|
|
|
/// <summary>
|
|
/// Bindable property for RowSpacing.
|
|
/// </summary>
|
|
public static readonly BindableProperty RowSpacingProperty =
|
|
BindableProperty.Create(
|
|
nameof(RowSpacing),
|
|
typeof(float),
|
|
typeof(SkiaGrid),
|
|
0f,
|
|
BindingMode.TwoWay,
|
|
propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure());
|
|
|
|
/// <summary>
|
|
/// Bindable property for ColumnSpacing.
|
|
/// </summary>
|
|
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<GridLength> _rowDefinitions = new();
|
|
private readonly List<GridLength> _columnDefinitions = new();
|
|
private readonly Dictionary<SkiaView, GridPosition> _childPositions = new();
|
|
|
|
private float[] _rowHeights = Array.Empty<float>();
|
|
private float[] _columnWidths = Array.Empty<float>();
|
|
|
|
/// <summary>
|
|
/// Gets the row definitions.
|
|
/// </summary>
|
|
public IList<GridLength> RowDefinitions => _rowDefinitions;
|
|
|
|
/// <summary>
|
|
/// Gets the column definitions.
|
|
/// </summary>
|
|
public IList<GridLength> ColumnDefinitions => _columnDefinitions;
|
|
|
|
/// <summary>
|
|
/// Spacing between rows.
|
|
/// </summary>
|
|
public float RowSpacing
|
|
{
|
|
get => (float)GetValue(RowSpacingProperty);
|
|
set => SetValue(RowSpacingProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spacing between columns.
|
|
/// </summary>
|
|
public float ColumnSpacing
|
|
{
|
|
get => (float)GetValue(ColumnSpacingProperty);
|
|
set => SetValue(ColumnSpacingProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a child at the specified grid position.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the grid position of a child.
|
|
/// </summary>
|
|
public GridPosition GetPosition(SkiaView child)
|
|
{
|
|
return _childPositions.TryGetValue(child, out var pos) ? pos : new GridPosition(0, 0, 1, 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the grid position of a child.
|
|
/// </summary>
|
|
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<GridLength> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grid position information.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grid length specification.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grid unit type options.
|
|
/// </summary>
|
|
public enum GridUnitType
|
|
{
|
|
Absolute,
|
|
Star,
|
|
Auto
|
|
}
|