// 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; /// /// Stack layout that arranges children in a horizontal or vertical line. /// public class SkiaStackLayout : SkiaLayoutView { /// /// Bindable property for Orientation. /// public static readonly BindableProperty OrientationProperty = BindableProperty.Create( nameof(Orientation), typeof(StackOrientation), typeof(SkiaStackLayout), StackOrientation.Vertical, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaStackLayout)b).InvalidateMeasure()); /// /// Gets or sets the orientation of the stack. /// 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; } } /// /// Stack orientation options. /// public enum StackOrientation { Vertical, Horizontal }