// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Microsoft.Maui;
namespace Microsoft.Maui.Platform;
///
/// Base class for layout containers that can arrange child views.
///
public abstract class SkiaLayoutView : SkiaView
{
private readonly List _children = new();
///
/// Gets the children of this layout.
///
public new IReadOnlyList Children => _children;
///
/// Spacing between children.
///
public float Spacing { get; set; } = 0;
///
/// Padding around the content.
///
public SKRect Padding { get; set; } = new SKRect(0, 0, 0, 0);
///
/// Gets or sets whether child views are clipped to the bounds.
///
public bool ClipToBounds { get; set; } = false;
///
/// Called when binding context changes. Propagates to layout children.
///
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
// Propagate binding context to layout children
foreach (var child in _children)
{
SetInheritedBindingContext(child, BindingContext);
}
}
///
/// Adds a child view.
///
public virtual void AddChild(SkiaView child)
{
if (child.Parent != null)
{
throw new InvalidOperationException("View already has a parent");
}
_children.Add(child);
child.Parent = this;
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
InvalidateMeasure();
Invalidate();
}
///
/// Removes a child view.
///
public virtual void RemoveChild(SkiaView child)
{
if (_children.Remove(child))
{
child.Parent = null;
InvalidateMeasure();
Invalidate();
}
}
///
/// Removes a child at the specified index.
///
public virtual void RemoveChildAt(int index)
{
if (index >= 0 && index < _children.Count)
{
var child = _children[index];
_children.RemoveAt(index);
child.Parent = null;
InvalidateMeasure();
Invalidate();
}
}
///
/// Inserts a child at the specified index.
///
public virtual void InsertChild(int index, SkiaView child)
{
if (child.Parent != null)
{
throw new InvalidOperationException("View already has a parent");
}
_children.Insert(index, child);
child.Parent = this;
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
InvalidateMeasure();
Invalidate();
}
///
/// Clears all children.
///
public virtual void ClearChildren()
{
foreach (var child in _children)
{
child.Parent = null;
}
_children.Clear();
InvalidateMeasure();
Invalidate();
}
///
/// Gets the content bounds (bounds minus padding).
///
protected virtual SKRect GetContentBounds()
{
return GetContentBounds(Bounds);
}
///
/// Gets the content bounds for a given bounds rectangle.
///
protected SKRect GetContentBounds(SKRect bounds)
{
return new SKRect(
bounds.Left + Padding.Left,
bounds.Top + Padding.Top,
bounds.Right - Padding.Right,
bounds.Bottom - Padding.Bottom);
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
// Draw background if set (for layouts inside CollectionView items)
if (BackgroundColor != SKColors.Transparent)
{
using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill };
canvas.DrawRect(bounds, bgPaint);
}
// Log for StackLayout
if (this is SkiaStackLayout)
{
bool hasCV = false;
foreach (var c in _children)
{
if (c is SkiaCollectionView) hasCV = true;
}
if (hasCV)
{
Console.WriteLine($"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}");
foreach (var c in _children)
{
Console.WriteLine($"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}");
}
}
}
// Draw children in order
foreach (var child in _children)
{
if (child.IsVisible)
{
child.Draw(canvas);
}
}
}
public override SkiaView? HitTest(float x, float y)
{
if (!IsVisible || !IsEnabled || !Bounds.Contains(new SKPoint(x, y)))
{
if (this is SkiaBorder)
{
Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={Bounds}, IsVisible={IsVisible}, IsEnabled={IsEnabled}");
}
return null;
}
// Hit test children in reverse order (top-most first)
for (int i = _children.Count - 1; i >= 0; i--)
{
var child = _children[i];
var hit = child.HitTest(x, y);
if (hit != null)
{
if (this is SkiaBorder)
{
Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={Bounds}, child={hit.GetType().Name}");
}
return hit;
}
}
if (this is SkiaBorder)
{
Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}");
}
return this;
}
///
/// Forward pointer pressed events to the appropriate child.
///
public override void OnPointerPressed(PointerEventArgs e)
{
// Find which child was hit and forward the event
var hit = HitTest(e.X, e.Y);
if (hit != null && hit != this)
{
hit.OnPointerPressed(e);
}
}
///
/// Forward pointer released events to the appropriate child.
///
public override void OnPointerReleased(PointerEventArgs e)
{
// Find which child was hit and forward the event
var hit = HitTest(e.X, e.Y);
if (hit != null && hit != this)
{
hit.OnPointerReleased(e);
}
}
///
/// Forward pointer moved events to the appropriate child.
///
public override void OnPointerMoved(PointerEventArgs e)
{
// Find which child was hit and forward the event
var hit = HitTest(e.X, e.Y);
if (hit != null && hit != this)
{
hit.OnPointerMoved(e);
}
}
///
/// Forward scroll events to the appropriate child.
///
public override void OnScroll(ScrollEventArgs e)
{
// Find which child was hit and forward the event
var hit = HitTest(e.X, e.Y);
if (hit != null && hit != this)
{
hit.OnScroll(e);
}
}
}
///
/// Stack layout that arranges children in a horizontal or vertical line.
///
public class SkiaStackLayout : SkiaLayoutView
{
///
/// Gets or sets the orientation of the stack.
///
public StackOrientation Orientation { get; set; } = StackOrientation.Vertical;
protected override SKSize MeasureOverride(SKSize availableSize)
{
// Handle NaN/Infinity in padding
var paddingLeft = float.IsNaN(Padding.Left) ? 0 : Padding.Left;
var paddingRight = float.IsNaN(Padding.Right) ? 0 : Padding.Right;
var paddingTop = float.IsNaN(Padding.Top) ? 0 : Padding.Top;
var paddingBottom = float.IsNaN(Padding.Bottom) ? 0 : Padding.Bottom;
var contentWidth = availableSize.Width - paddingLeft - paddingRight;
var contentHeight = 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;
var childAvailable = new SKSize(contentWidth, contentHeight);
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var childSize = child.Measure(childAvailable);
// Skip NaN sizes from child measurements
var childWidth = float.IsNaN(childSize.Width) ? 0 : childSize.Width;
var childHeight = float.IsNaN(childSize.Height) ? 0 : 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 = Math.Max(0, visibleCount - 1) * Spacing;
if (Orientation == StackOrientation.Vertical)
{
totalHeight += totalSpacing;
return new SKSize(
maxWidth + paddingLeft + paddingRight,
totalHeight + paddingTop + paddingBottom);
}
else
{
totalWidth += totalSpacing;
return new SKSize(
totalWidth + paddingLeft + paddingRight,
maxHeight + paddingTop + paddingBottom);
}
}
protected override SKRect ArrangeOverride(SKRect bounds)
{
var content = GetContentBounds(bounds);
// 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 = float.IsNaN(childDesired.Width) || float.IsInfinity(childDesired.Width)
? contentWidth
: childDesired.Width;
var childHeight = float.IsNaN(childDesired.Height) || float.IsInfinity(childDesired.Height)
? contentHeight
: childDesired.Height;
SKRect childBounds;
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);
childBounds = new SKRect(
content.Left,
content.Top + offset,
content.Left + contentWidth,
content.Top + offset + useHeight);
offset += useHeight + Spacing;
}
else
{
// For ScrollView children, give them the remaining viewport width
var remainingWidth = Math.Max(0, contentWidth - offset);
var useWidth = child is SkiaScrollView
? remainingWidth
: Math.Min(childWidth, remainingWidth > 0 ? remainingWidth : childWidth);
// Respect child's VerticalOptions for horizontal layouts
var useHeight = Math.Min(childHeight, contentHeight);
float childTop = content.Top;
float childBottom = 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;
childBottom = childTop + useHeight;
}
else if (alignmentValue == 2) // End
{
childTop = content.Top + contentHeight - useHeight;
childBottom = content.Top + contentHeight;
}
else if (alignmentValue == 3) // Fill
{
childTop = content.Top;
childBottom = content.Top + contentHeight;
}
childBounds = new SKRect(
content.Left + offset,
childTop,
content.Left + offset + useWidth,
childBottom);
offset += useWidth + Spacing;
}
// Apply child's margin
var margin = child.Margin;
var marginedBounds = new SKRect(
childBounds.Left + (float)margin.Left,
childBounds.Top + (float)margin.Top,
childBounds.Right - (float)margin.Right,
childBounds.Bottom - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
}
}
///
/// Stack orientation options.
///
public enum StackOrientation
{
Vertical,
Horizontal
}
///
/// Grid layout that arranges children in rows and columns.
///
public class SkiaGrid : SkiaLayoutView
{
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; set; } = 0;
///
/// Spacing between columns.
///
public float ColumnSpacing { get; set; } = 0;
///
/// 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 SKSize MeasureOverride(SKSize availableSize)
{
var contentWidth = availableSize.Width - Padding.Left - Padding.Right;
var contentHeight = 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 SKSize(float.PositiveInfinity, float.PositiveInfinity));
var childWidth = float.IsNaN(childSize.Width) ? 0 : 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 SKSize(cellWidth, float.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 = 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 SKSize(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 SKSize(
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 SKRect ArrangeOverride(SKRect bounds)
{
try
{
var content = GetContentBounds(bounds);
// 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 marginedBounds = new SKRect(
x + (float)margin.Left,
y + (float)margin.Top,
x + width - (float)margin.Right,
y + height - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
}
catch (Exception ex)
{
Console.WriteLine($"[SkiaGrid] EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}");
Console.WriteLine($"[SkiaGrid] Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}");
Console.WriteLine($"[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
}
///
/// Absolute layout that positions children at exact coordinates.
///
public class SkiaAbsoluteLayout : SkiaLayoutView
{
private readonly Dictionary _childBounds = new();
///
/// Adds a child at the specified position and size.
///
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);
}
///
/// Gets the layout bounds for a child.
///
public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child)
{
return _childBounds.TryGetValue(child, out var bounds)
? bounds
: new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None);
}
///
/// Sets the layout bounds for a child.
///
public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None)
{
_childBounds[child] = new AbsoluteLayoutBounds(bounds, flags);
InvalidateMeasure();
Invalidate();
}
protected override SKSize MeasureOverride(SKSize 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 SKSize(bounds.Width, bounds.Height));
maxRight = Math.Max(maxRight, bounds.Right);
maxBottom = Math.Max(maxBottom, bounds.Bottom);
}
return new SKSize(
maxRight + Padding.Left + Padding.Right,
maxBottom + Padding.Top + Padding.Bottom);
}
protected override SKRect ArrangeOverride(SKRect bounds)
{
var content = GetContentBounds(bounds);
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 = child.DesiredSize.Width;
else
width = childBounds.Width;
// Height
if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional))
height = childBounds.Height * content.Height;
else if (childBounds.Height < 0)
height = child.DesiredSize.Height;
else
height = childBounds.Height;
// Apply child's margin
var margin = child.Margin;
var marginedBounds = new SKRect(
x + (float)margin.Left,
y + (float)margin.Top,
x + width - (float)margin.Right,
y + height - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
}
}
///
/// Absolute layout bounds for a child.
///
public readonly struct AbsoluteLayoutBounds
{
public SKRect Bounds { get; }
public AbsoluteLayoutFlags Flags { get; }
public AbsoluteLayoutBounds(SKRect bounds, AbsoluteLayoutFlags flags)
{
Bounds = bounds;
Flags = flags;
}
}
///
/// Flags for absolute layout positioning.
///
[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
}