refactor: split large files into partial classes by concern

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.
This commit is contained in:
2026-03-06 22:36:23 -05:00
parent ee60b983a4
commit 077abc2feb
20 changed files with 4693 additions and 4577 deletions

View File

@@ -315,860 +315,3 @@ public abstract class SkiaLayoutView : SkiaView
}
}
}
/// <summary>
/// Stack layout that arranges children in a horizontal or vertical line.
/// </summary>
public class SkiaStackLayout : SkiaLayoutView
{
/// <summary>
/// Bindable property for Orientation.
/// </summary>
public static readonly BindableProperty OrientationProperty =
BindableProperty.Create(
nameof(Orientation),
typeof(StackOrientation),
typeof(SkiaStackLayout),
StackOrientation.Vertical,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStackLayout)b).InvalidateMeasure());
/// <summary>
/// Gets or sets the orientation of the stack.
/// </summary>
public StackOrientation Orientation
{
get => (StackOrientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
// Handle NaN/Infinity in padding
var paddingLeft = (float)(double.IsNaN(Padding.Left) ? 0 : Padding.Left);
var paddingRight = (float)(double.IsNaN(Padding.Right) ? 0 : Padding.Right);
var paddingTop = (float)(double.IsNaN(Padding.Top) ? 0 : Padding.Top);
var paddingBottom = (float)(double.IsNaN(Padding.Bottom) ? 0 : Padding.Bottom);
var contentWidth = (float)availableSize.Width - paddingLeft - paddingRight;
var contentHeight = (float)availableSize.Height - paddingTop - paddingBottom;
// Clamp negative sizes to 0
if (contentWidth < 0 || float.IsNaN(contentWidth)) contentWidth = 0;
if (contentHeight < 0 || float.IsNaN(contentHeight)) contentHeight = 0;
float totalWidth = 0;
float totalHeight = 0;
float maxWidth = 0;
float maxHeight = 0;
// For stack layouts, give children infinite size in the stacking direction
// so they can measure to their natural size
var childAvailable = Orientation == StackOrientation.Horizontal
? new Size(double.PositiveInfinity, contentHeight) // Horizontal: infinite width, constrained height
: new Size(contentWidth, double.PositiveInfinity); // Vertical: constrained width, infinite height
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var childSize = child.Measure(childAvailable);
// Skip NaN sizes from child measurements
var childWidth = double.IsNaN(childSize.Width) ? 0f : (float)childSize.Width;
var childHeight = double.IsNaN(childSize.Height) ? 0f : (float)childSize.Height;
if (Orientation == StackOrientation.Vertical)
{
totalHeight += childHeight;
maxWidth = Math.Max(maxWidth, childWidth);
}
else
{
totalWidth += childWidth;
maxHeight = Math.Max(maxHeight, childHeight);
}
}
// Add spacing
var visibleCount = Children.Count(c => c.IsVisible);
var totalSpacing = (float)(Math.Max(0, visibleCount - 1) * Spacing);
if (Orientation == StackOrientation.Vertical)
{
totalHeight += totalSpacing;
return new Size(
maxWidth + paddingLeft + paddingRight,
totalHeight + paddingTop + paddingBottom);
}
else
{
totalWidth += totalSpacing;
return new Size(
totalWidth + paddingLeft + paddingRight,
maxHeight + paddingTop + paddingBottom);
}
}
protected override Rect ArrangeOverride(Rect bounds)
{
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
// Clamp content dimensions if infinite - use reasonable defaults
var contentWidth = float.IsInfinity(content.Width) || float.IsNaN(content.Width) ? 800f : content.Width;
var contentHeight = float.IsInfinity(content.Height) || float.IsNaN(content.Height) ? 600f : content.Height;
float offset = 0;
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var childDesired = child.DesiredSize;
// Handle NaN and Infinity in desired size
var childWidth = double.IsNaN(childDesired.Width) || double.IsInfinity(childDesired.Width)
? contentWidth
: (float)childDesired.Width;
var childHeight = double.IsNaN(childDesired.Height) || double.IsInfinity(childDesired.Height)
? contentHeight
: (float)childDesired.Height;
float childBoundsLeft, childBoundsTop, childBoundsWidth, childBoundsHeight;
if (Orientation == StackOrientation.Vertical)
{
// For ScrollView children, give them the remaining viewport height
// Clamp to avoid giving them their content size
var remainingHeight = Math.Max(0, contentHeight - offset);
var useHeight = child is SkiaScrollView
? remainingHeight
: Math.Min(childHeight, remainingHeight > 0 ? remainingHeight : childHeight);
// Respect child's HorizontalOptions for vertical layouts
var useWidth = Math.Min(childWidth, contentWidth);
float childLeft = content.Left;
var horizontalOptions = child.HorizontalOptions;
var alignmentValue = (int)horizontalOptions.Alignment;
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
if (alignmentValue == 1) // Center
{
childLeft = content.Left + (contentWidth - useWidth) / 2;
}
else if (alignmentValue == 2) // End
{
childLeft = content.Left + contentWidth - useWidth;
}
else if (alignmentValue == 3) // Fill
{
useWidth = contentWidth;
}
childBoundsLeft = childLeft;
childBoundsTop = content.Top + offset;
childBoundsWidth = useWidth;
childBoundsHeight = useHeight;
offset += useHeight + (float)Spacing;
}
else
{
// Horizontal stack: give each child its measured width
// Don't constrain - let content overflow if needed (parent clips)
var useWidth = childWidth;
// Respect child's VerticalOptions for horizontal layouts
var useHeight = Math.Min(childHeight, contentHeight);
float childTop = content.Top;
float childBottomCalc = content.Top + useHeight;
var verticalOptions = child.VerticalOptions;
var alignmentValue = (int)verticalOptions.Alignment;
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
if (alignmentValue == 1) // Center
{
childTop = content.Top + (contentHeight - useHeight) / 2;
childBottomCalc = childTop + useHeight;
}
else if (alignmentValue == 2) // End
{
childTop = content.Top + contentHeight - useHeight;
childBottomCalc = content.Top + contentHeight;
}
else if (alignmentValue == 3) // Fill
{
childTop = content.Top;
childBottomCalc = content.Top + contentHeight;
}
childBoundsLeft = content.Left + offset;
childBoundsTop = childTop;
childBoundsWidth = useWidth;
childBoundsHeight = childBottomCalc - childTop;
offset += useWidth + (float)Spacing;
}
// Apply child's margin
var margin = child.Margin;
var marginedBounds = new Rect(
childBoundsLeft + (float)margin.Left,
childBoundsTop + (float)margin.Top,
childBoundsWidth - (float)margin.Left - (float)margin.Right,
childBoundsHeight - (float)margin.Top - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
}
}
/// <summary>
/// Stack orientation options.
/// </summary>
public enum StackOrientation
{
Vertical,
Horizontal
}
/// <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
}
/// <summary>
/// Absolute layout that positions children at exact coordinates.
/// </summary>
public class SkiaAbsoluteLayout : SkiaLayoutView
{
private readonly Dictionary<SkiaView, AbsoluteLayoutBounds> _childBounds = new();
/// <summary>
/// Adds a child at the specified position and size.
/// </summary>
public void AddChild(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
{
base.AddChild(child);
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
}
public override void RemoveChild(SkiaView child)
{
base.RemoveChild(child);
_childBounds.Remove(child);
}
/// <summary>
/// Gets the layout bounds for a child.
/// </summary>
public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child)
{
return _childBounds.TryGetValue(child, out var bounds)
? bounds
: new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None);
}
/// <summary>
/// Sets the layout bounds for a child.
/// </summary>
public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
{
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
InvalidateMeasure();
Invalidate();
}
protected override Size MeasureOverride(Size availableSize)
{
float maxRight = 0;
float maxBottom = 0;
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var layout = GetLayoutBounds(child);
var bounds = layout.Bounds;
child.Measure(new Size(bounds.Width, bounds.Height));
maxRight = Math.Max(maxRight, bounds.Right);
maxBottom = Math.Max(maxBottom, bounds.Bottom);
}
return new Size(
maxRight + Padding.Left + Padding.Right,
maxBottom + Padding.Top + Padding.Bottom);
}
protected override Rect ArrangeOverride(Rect bounds)
{
var content = GetContentBounds(new SKRect((float)bounds.Left, (float)bounds.Top, (float)bounds.Right, (float)bounds.Bottom));
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var layout = GetLayoutBounds(child);
var childBounds = layout.Bounds;
var flags = layout.Flags;
float x, y, width, height;
// X position
if (flags.HasFlag(AbsoluteLayoutFlags.XProportional))
x = content.Left + childBounds.Left * content.Width;
else
x = content.Left + childBounds.Left;
// Y position
if (flags.HasFlag(AbsoluteLayoutFlags.YProportional))
y = content.Top + childBounds.Top * content.Height;
else
y = content.Top + childBounds.Top;
// Width
if (flags.HasFlag(AbsoluteLayoutFlags.WidthProportional))
width = childBounds.Width * content.Width;
else if (childBounds.Width < 0)
width = (float)child.DesiredSize.Width;
else
width = childBounds.Width;
// Height
if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional))
height = childBounds.Height * content.Height;
else if (childBounds.Height < 0)
height = (float)child.DesiredSize.Height;
else
height = childBounds.Height;
// Apply child's margin
var margin = child.Margin;
var marginedBounds = new Rect(
x + (float)margin.Left,
y + (float)margin.Top,
width - (float)margin.Left - (float)margin.Right,
height - (float)margin.Top - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
}
}
/// <summary>
/// Absolute layout bounds for a child.
/// </summary>
public readonly struct AbsoluteLayoutBounds
{
public SKRect Bounds { get; }
public AbsoluteLayoutFlags Flags { get; }
public AbsoluteLayoutBounds(SKRect bounds, AbsoluteLayoutFlags flags)
{
Bounds = bounds;
Flags = flags;
}
}
/// <summary>
/// Flags for absolute layout positioning.
/// </summary>
[Flags]
public enum AbsoluteLayoutFlags
{
None = 0,
XProportional = 1,
YProportional = 2,
WidthProportional = 4,
HeightProportional = 8,
PositionProportional = XProportional | YProportional,
SizeProportional = WidthProportional | HeightProportional,
All = XProportional | YProportional | WidthProportional | HeightProportional
}