Implement LayoutAlignment for Core layouts (#505)

* Implement LayoutAlignment in Core

* Fix parameter name
This commit is contained in:
E.Z. Hart 2021-03-24 14:40:40 -06:00 коммит произвёл GitHub
Родитель b7012dbdb2
Коммит 613dbc0072
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 314 добавлений и 103 удалений

Просмотреть файл

@ -40,8 +40,8 @@ namespace Maui.Controls.Sample.Pages
label.Margin = new Thickness(15, 10, 20, 15);
verticalStack.Add(label);
verticalStack.Add(new Label { Text = "This should be BIG text!", FontSize = 24 });
verticalStack.Add(new Label { Text = "This should be BOLD text!", FontAttributes = FontAttributes.Bold });
verticalStack.Add(new Label { Text = "This should be BIG text!", FontSize = 24, HorizontalOptions = LayoutOptions.End });
verticalStack.Add(new Label { Text = "This should be BOLD text!", FontAttributes = FontAttributes.Bold, HorizontalOptions = LayoutOptions.Center });
verticalStack.Add(new Label { Text = "This should be a CUSTOM font!", FontFamily = "Dokdo" });
verticalStack.Add(new Label { Text = "This should have padding", Padding = new Thickness(40), BackgroundColor = Color.LightBlue });
verticalStack.Add(new Label { Text = loremIpsum });
@ -77,7 +77,7 @@ namespace Maui.Controls.Sample.Pages
horizontalStack.Add(button);
horizontalStack.Add(button2);
horizontalStack.Add(new Label { Text = "And these buttons are in a HorizontalStackLayout" });
horizontalStack.Add(new Label { Text = "And these buttons are in a HorizontalStackLayout", VerticalOptions = LayoutOptions.Center });
verticalStack.Add(horizontalStack);

Просмотреть файл

@ -16,6 +16,8 @@ namespace Microsoft.Maui.Controls
Thickness Maui.IView.Margin => new Thickness();
public Primitives.LayoutAlignment HorizontalLayoutAlignment => Primitives.LayoutAlignment.Fill;
void Maui.ILayout.Add(IView child)
{
Content = (View)child;
@ -61,5 +63,7 @@ namespace Microsoft.Maui.Controls
layout.ResolveLayoutChanges();
}
}
}

Просмотреть файл

@ -123,5 +123,7 @@ namespace Microsoft.Maui.Controls
}
Maui.FlowDirection IFrameworkElement.FlowDirection => FlowDirection.ToPlatformFlowDirection();
Primitives.LayoutAlignment IFrameworkElement.HorizontalLayoutAlignment => default;
Primitives.LayoutAlignment IFrameworkElement.VerticalLayoutAlignment => default;
}
}

Просмотреть файл

@ -35,5 +35,22 @@ namespace Microsoft.Maui.Controls
get { return (_flags & (int)LayoutExpandFlag.Expand) != 0; }
set { _flags = (_flags & 3) | (value ? (int)LayoutExpandFlag.Expand : 0); }
}
internal Primitives.LayoutAlignment ToCore()
{
switch (Alignment)
{
case LayoutAlignment.Start:
return Primitives.LayoutAlignment.Start;
case LayoutAlignment.Center:
return Primitives.LayoutAlignment.Center;
case LayoutAlignment.End:
return Primitives.LayoutAlignment.End;
case LayoutAlignment.Fill:
return Primitives.LayoutAlignment.Fill;
}
return Primitives.LayoutAlignment.Start;
}
}
}

Просмотреть файл

@ -195,6 +195,9 @@ namespace Microsoft.Maui.Controls
double IFrameworkElement.Width { get => WidthRequest; }
double IFrameworkElement.Height { get => HeightRequest; }
Primitives.LayoutAlignment IFrameworkElement.HorizontalLayoutAlignment => HorizontalOptions.ToCore();
Primitives.LayoutAlignment IFrameworkElement.VerticalLayoutAlignment => VerticalOptions.ToCore();
#endregion
}
}

Просмотреть файл

@ -1,4 +1,6 @@
namespace Microsoft.Maui
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui
{
/// <summary>
/// Represents a framework-level set of properties, events, and methods for .NET MAUI elements.
@ -88,5 +90,15 @@
/// Direction in which the UI elements on the page are scanned by the eye
/// </summary>
FlowDirection FlowDirection { get; }
/// <summary>
/// Determines the horizontal aspect of this element's arrangement in a container
/// </summary>
LayoutAlignment HorizontalLayoutAlignment { get; }
/// <summary>
/// Determines the vertical aspect of this element's arrangement in a container
/// </summary>
LayoutAlignment VerticalLayoutAlignment { get; }
}
}

Просмотреть файл

@ -59,8 +59,8 @@ namespace Microsoft.Maui.Handlers
var deviceWidthConstraint = Context.ToPixels(widthConstraint);
var deviceHeightConstraint = Context.ToPixels(heightConstraint);
var widthSpec = MeasureSpecMode.Exactly.MakeMeasureSpec((int)deviceWidthConstraint);
var heightSpec = MeasureSpecMode.Exactly.MakeMeasureSpec((int)deviceHeightConstraint);
var widthSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceWidthConstraint);
var heightSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceHeightConstraint);
TypedNativeView.Measure(widthSpec, heightSpec);

Просмотреть файл

@ -20,17 +20,17 @@ namespace Microsoft.Maui.Layouts
return new Size(finalWidth, measure.Height);
}
public override void ArrangeChildren(Rectangle childBounds)
public override void ArrangeChildren(Rectangle bounds)
{
if (Stack.FlowDirection == FlowDirection.LeftToRight)
{
ArrangeLeftToRight(Stack.Spacing, Stack.Children);
ArrangeLeftToRight(bounds.Height, Stack.Spacing, Stack.Children);
}
else
{
// We _could_ simply reverse the list of child views when arranging from right to left,
// but this way we avoid extra list and enumerator allocations
ArrangeRightToLeft(Stack.Spacing, Stack.Children);
ArrangeRightToLeft(bounds.Height, Stack.Spacing, Stack.Children);
}
}
@ -53,31 +53,31 @@ namespace Microsoft.Maui.Layouts
return new Size(totalRequestedWidth, requestedHeight);
}
static void ArrangeLeftToRight(int spacing, IReadOnlyList<IView> views)
static void ArrangeLeftToRight(double height, int spacing, IReadOnlyList<IView> views)
{
double xPosition = 0;
for (int n = 0; n < views.Count; n++)
{
var child = views[n];
xPosition += ArrangeChild(child, spacing, xPosition);
xPosition += ArrangeChild(child, height, spacing, xPosition);
}
}
static void ArrangeRightToLeft(int spacing, IReadOnlyList<IView> views)
static void ArrangeRightToLeft(double height, int spacing, IReadOnlyList<IView> views)
{
double xPostition = 0;
for (int n = views.Count - 1; n >= 0; n--)
{
var child = views[n];
xPostition += ArrangeChild(child, spacing, xPostition);
xPostition += ArrangeChild(child, height, spacing, xPostition);
}
}
static double ArrangeChild(IView child, int spacing, double x)
static double ArrangeChild(IView child, double height, int spacing, double x)
{
var destination = new Rectangle(x, 0, child.DesiredSize.Width, child.DesiredSize.Height);
var destination = new Rectangle(x, 0, child.DesiredSize.Width, height);
child.Arrange(destination);
return destination.Width + spacing;
}

Просмотреть файл

@ -1,5 +1,5 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui.Layouts
{
@ -36,16 +36,18 @@ namespace Microsoft.Maui.Layouts
{
Thickness margin = frameworkElement.GetMargin();
// If the margins are too big for the bounds, then simply collapse them to zero
var frameWidth = Math.Max(0, bounds.Width - margin.HorizontalThickness);
var frameHeight = Math.Max(0, bounds.Height - margin.VerticalThickness);
var frameWidth = frameworkElement.HorizontalLayoutAlignment == LayoutAlignment.Fill
? Math.Max(0, bounds.Width - margin.HorizontalThickness)
: frameworkElement.DesiredSize.Width;
var xMarginAdjustment = frameworkElement.FlowDirection == FlowDirection.LeftToRight
? margin.Left
: margin.Right;
var frameHeight = frameworkElement.VerticalLayoutAlignment == LayoutAlignment.Fill
? Math.Max(0, bounds.Height - margin.VerticalThickness)
: frameworkElement.DesiredSize.Height;
return new Rectangle(bounds.X + xMarginAdjustment, bounds.Y + margin.Top,
frameWidth, frameHeight);
var frameX = AlignHorizontal(frameworkElement, bounds, margin);
var frameY = AlignVertical(frameworkElement, bounds, margin);
return new Rectangle(frameX, frameY, frameWidth, frameHeight);
}
static Thickness GetMargin(this IFrameworkElement frameworkElement)
@ -53,7 +55,94 @@ namespace Microsoft.Maui.Layouts
if (frameworkElement is IView view)
return view.Margin;
return new Thickness();
return Thickness.Zero;
}
static double AlignHorizontal(IFrameworkElement frameworkElement, Rectangle bounds, Thickness margin)
{
var alignment = frameworkElement.HorizontalLayoutAlignment;
var desiredWidth = frameworkElement.DesiredSize.Width;
var startX = bounds.X;
if (frameworkElement.FlowDirection == FlowDirection.LeftToRight)
{
return AlignHorizontal(startX, margin.Left, margin.Right, bounds.Width, desiredWidth, alignment);
}
// If the flowdirection is RTL, then we can use the same logic to determine the X position of the Frame;
// we just have to flip a few parameters. First we flip the alignment if it's start or end:
if (alignment == LayoutAlignment.End)
{
alignment = LayoutAlignment.Start;
}
else if (alignment == LayoutAlignment.Start)
{
alignment = LayoutAlignment.End;
}
// And then we swap the left and right margins:
return AlignHorizontal(startX, margin.Right, margin.Left, bounds.Width, desiredWidth, alignment);
}
static double AlignHorizontal(double startX, double startMargin, double endMargin, double boundsWidth,
double desiredWidth, LayoutAlignment horizontalLayoutAlignment)
{
double frameX = 0;
switch (horizontalLayoutAlignment)
{
case LayoutAlignment.Fill:
case LayoutAlignment.Start:
frameX = startX + startMargin;
break;
case LayoutAlignment.Center:
frameX = (boundsWidth - desiredWidth) / 2;
var marginOffset = (startMargin - endMargin) / 2;
frameX += marginOffset;
break;
case LayoutAlignment.End:
frameX = boundsWidth - endMargin - desiredWidth;
break;
}
return frameX;
}
static double AlignVertical(IFrameworkElement frameworkElement, Rectangle bounds, Thickness margin)
{
double frameY = 0;
switch (frameworkElement.VerticalLayoutAlignment)
{
case LayoutAlignment.Fill:
frameY = bounds.Y + margin.Top;
break;
case LayoutAlignment.Start:
frameY = bounds.Y + margin.Top;
break;
case LayoutAlignment.Center:
frameY = (bounds.Height - frameworkElement.DesiredSize.Height) / 2;
var offset = (margin.Top - margin.Bottom) / 2;
frameY += offset;
break;
case LayoutAlignment.End:
frameY = bounds.Height - margin.Bottom - frameworkElement.DesiredSize.Height;
break;
}
return frameY;
}
}
}

Просмотреть файл

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.Maui;
namespace Microsoft.Maui.Layouts
{
@ -21,7 +20,7 @@ namespace Microsoft.Maui.Layouts
return new Size(measure.Width, finalHeight);
}
public override void ArrangeChildren(Rectangle childBounds) => Arrange(Stack.Spacing, Stack.Children);
public override void ArrangeChildren(Rectangle bounds) => Arrange(bounds.Width, Stack.Spacing, Stack.Children);
static Size Measure(double widthConstraint, int spacing, IReadOnlyList<IView> views)
{
@ -41,13 +40,13 @@ namespace Microsoft.Maui.Layouts
return new Size(requestedWidth, totalRequestedHeight);
}
static void Arrange(int spacing, IEnumerable<IView> views)
static void Arrange(double width, int spacing, IEnumerable<IView> views)
{
double stackHeight = 0;
foreach (var child in views)
{
var destination = new Rectangle(0, stackHeight, child.DesiredSize.Width, child.DesiredSize.Height);
var destination = new Rectangle(0, stackHeight, width, child.DesiredSize.Height);
child.Arrange(destination);
stackHeight += destination.Height + spacing;
}

Просмотреть файл

@ -0,0 +1,30 @@
namespace Microsoft.Maui.Primitives
{
// We don't use Microsoft.Maui.Controls.LayoutAlignment directly because it has a Flags attribute, which we do not want
/// <summary>
/// Determines the position and size of an IFrameworkElement when arranged in a parent element
/// </summary>
public enum LayoutAlignment
{
/// <summary>
/// Fill the available space
/// </summary>
Fill,
/// <summary>
/// Align with the leading edge of the available space, as determined by FlowDirection
/// </summary>
Start,
/// <summary>
/// Center in the available space
/// </summary>
Center,
/// <summary>
/// Align with the trailing edge of the available space, as determined by FlowDirection
/// </summary>
End
}
}

Просмотреть файл

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui.DeviceTests.Stubs
{
@ -32,6 +33,10 @@ namespace Microsoft.Maui.DeviceTests.Stubs
public FlowDirection FlowDirection { get; set; }
public LayoutAlignment HorizontalLayoutAlignment { get; set; }
public LayoutAlignment VerticalLayoutAlignment { get; set; }
public void Arrange(Rectangle bounds)
{
Frame = bounds;

Просмотреть файл

@ -3,6 +3,7 @@ using Microsoft.Maui;
using Microsoft.Maui.Layouts;
using NSubstitute;
using Xunit;
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui.UnitTests.Layouts
{
@ -82,33 +83,6 @@ namespace Microsoft.Maui.UnitTests.Layouts
Assert.Equal(expectedWidth, measurement.Width);
}
[Fact]
public void ViewsArrangedWithDesiredHeights()
{
var stack = CreateTestLayout();
var manager = new HorizontalStackLayoutManager(stack);
var view1 = LayoutTestHelpers.CreateTestView(new Size(100, 200));
var view2 = LayoutTestHelpers.CreateTestView(new Size(100, 150));
var children = new List<IView>() { view1, view2 }.AsReadOnly();
stack.Children.Returns(children);
var measurement = manager.Measure(double.PositiveInfinity, double.PositiveInfinity);
manager.ArrangeChildren(new Rectangle(Point.Zero, measurement));
// The tallest IView is 200, so the stack should be that tall
Assert.Equal(200, measurement.Height);
// We expect the first IView to be at 0,0 with a width of 100 and a height of 200
var expectedRectangle1 = new Rectangle(0, 0, 100, 200);
view1.Received().Arrange(Arg.Is(expectedRectangle1));
// We expect the second IView to be at 100, 0 with a width of 100 and a height of 150
var expectedRectangle2 = new Rectangle(100, 0, 100, 150);
view2.Received().Arrange(Arg.Is(expectedRectangle2));
}
[Fact(DisplayName = "First View in LTR Horizontal Stack is on the left")]
public void LtrShouldHaveFirstItemOnTheLeft()
{

Просмотреть файл

@ -1,7 +1,8 @@
using Microsoft.Maui;
using Microsoft.Maui.Layouts;
using Microsoft.Maui.Layouts;
using NSubstitute;
using Xunit;
using Microsoft.Maui.Primitives;
using System.Collections.Generic;
namespace Microsoft.Maui.UnitTests.Layouts
{
@ -27,12 +28,18 @@ namespace Microsoft.Maui.UnitTests.Layouts
Assert.Equal(60, frame.Height);
}
[Fact]
public void FrameSizeGoesToZeroWhenMarginsExceedBounds()
[Theory]
[InlineData(LayoutAlignment.Fill)]
[InlineData(LayoutAlignment.Start)]
[InlineData(LayoutAlignment.Center)]
[InlineData(LayoutAlignment.End)]
public void FrameSizeGoesToZeroWhenMarginsExceedBounds(LayoutAlignment layoutAlignment)
{
var element = Substitute.For<IView>();
var margin = new Thickness(200);
element.Margin.Returns(margin);
element.HorizontalLayoutAlignment.Returns(layoutAlignment);
element.VerticalLayoutAlignment.Returns(layoutAlignment);
var bounds = new Rectangle(0, 0, 100, 100);
var frame = element.ComputeFrame(bounds);
@ -69,27 +76,118 @@ namespace Microsoft.Maui.UnitTests.Layouts
Assert.Equal(90, desiredSize.Height);
}
[Fact]
public void MarginsAccountForFlowDirectionRTL()
public static IEnumerable<object[]> AlignmentTestData()
{
var margin = Thickness.Zero;
// No margin
yield return new object[] { LayoutAlignment.Start, margin, 0, 100};
yield return new object[] { LayoutAlignment.Center, margin, 100, 100 };
yield return new object[] { LayoutAlignment.End, margin, 200, 100};
yield return new object[] { LayoutAlignment.Fill, margin, 0, 300};
// Even margin
margin = new Thickness(10);
yield return new object[] { LayoutAlignment.Start, margin, 10, 100};
yield return new object[] { LayoutAlignment.Center, margin, 100, 100};
yield return new object[] { LayoutAlignment.End, margin, 190, 100};
yield return new object[] { LayoutAlignment.Fill, margin, 10, 280 };
// Lopsided margin
margin = new Thickness(5, 5, 10, 10);
yield return new object[] { LayoutAlignment.Start, margin, 5, 100};
yield return new object[] { LayoutAlignment.Center, margin, 97.5, 100 };
yield return new object[] { LayoutAlignment.End, margin, 190, 100 };
yield return new object[] { LayoutAlignment.Fill, margin, 5, 285 };
}
[Theory]
[MemberData(nameof(AlignmentTestData))]
public void FrameAccountsForHorizontalLayoutAlignment(LayoutAlignment layoutAlignment, Thickness margin,
double expectedX, double expectedWidth)
{
var widthConstraint = 300;
var heightConstraint = 50;
var viewSize = new Size(100, 50);
var element = Substitute.For<IView>();
element.FlowDirection.Returns(FlowDirection.RightToLeft);
var margin = new Thickness(10, 0, 15, 0);
element.Margin.Returns(margin);
element.DesiredSize.Returns(viewSize);
element.HorizontalLayoutAlignment.Returns(layoutAlignment);
var bounds = new Rectangle(0, 0, 100, 100);
var frame = element.ComputeFrame(bounds);
var frame = element.ComputeFrame(new Rectangle(0, 0, widthConstraint, heightConstraint));
// The top and bottom margins are zero, so we expect the Frame to have the full height of 100
Assert.Equal(0, frame.Top);
Assert.Equal(100, frame.Height);
Assert.Equal(expectedX, frame.Left);
Assert.Equal(expectedWidth, frame.Width);
}
// The left and right margins together are 25, so we expect the Frame to have a width of 75
Assert.Equal(75, frame.Width);
[Theory]
[MemberData(nameof(AlignmentTestData))]
public void FrameAccountsForVerticalLayoutAlignment(LayoutAlignment layoutAlignment, Thickness margin,
double expectedY, double expectedHeight)
{
var widthConstraint = 50;
var heightConstraint = 300;
var viewSize = new Size(50, 100);
// The left and right margins should be swapped (because of RTL) so we expect the Frame location
// to have a Left value of 15 (the "right side" margin)
Assert.Equal(15, frame.Left);
var element = Substitute.For<IView>();
element.Margin.Returns(margin);
element.DesiredSize.Returns(viewSize);
element.VerticalLayoutAlignment.Returns(layoutAlignment);
var frame = element.ComputeFrame(new Rectangle(0, 0, widthConstraint, heightConstraint));
Assert.Equal(expectedY, frame.Top);
Assert.Equal(expectedHeight, frame.Height);
}
public static IEnumerable<object[]> AlignmentTestDataRtl()
{
var margin = Thickness.Zero;
// No margin
yield return new object[] { LayoutAlignment.Start, margin, 200, 100 };
yield return new object[] { LayoutAlignment.Center, margin, 100, 100 };
yield return new object[] { LayoutAlignment.End, margin, 0, 100 };
yield return new object[] { LayoutAlignment.Fill, margin, 0, 300 };
// Even margin
margin = new Thickness(10);
yield return new object[] { LayoutAlignment.Start, margin, 190, 100 };
yield return new object[] { LayoutAlignment.Center, margin, 100, 100 };
yield return new object[] { LayoutAlignment.End, margin, 10, 100 };
yield return new object[] { LayoutAlignment.Fill, margin, 10, 280 };
// Lopsided margin
margin = new Thickness(5, 5, 10, 10);
yield return new object[] { LayoutAlignment.Start, margin, 195, 100 };
yield return new object[] { LayoutAlignment.Center, margin, 102.5, 100 };
yield return new object[] { LayoutAlignment.End, margin, 10, 100 };
yield return new object[] { LayoutAlignment.Fill, margin, 10, 285 };
}
[Theory]
[MemberData(nameof(AlignmentTestDataRtl))]
public void FrameAccountsForHorizontalLayoutAlignmentRtl(LayoutAlignment layoutAlignment, Thickness margin,
double expectedX, double expectedWidth)
{
var widthConstraint = 300;
var heightConstraint = 50;
var viewSize = new Size(100, 50);
var element = Substitute.For<IView>();
element.Margin.Returns(margin);
element.DesiredSize.Returns(viewSize);
element.FlowDirection.Returns(FlowDirection.RightToLeft);
element.HorizontalLayoutAlignment.Returns(layoutAlignment);
var frame = element.ComputeFrame(new Rectangle(0, 0, widthConstraint, heightConstraint));
Assert.Equal(expectedX, frame.Left);
Assert.Equal(expectedWidth, frame.Width);
}
}
}

Просмотреть файл

@ -29,9 +29,11 @@ namespace Microsoft.Maui.UnitTests.Layouts
protected IView CreateTestView(Size viewSize)
{
var view = CreateTestView();
var handler = Substitute.For<IViewHandler>();
view.Measure(Arg.Any<double>(), Arg.Any<double>()).Returns(viewSize);
view.DesiredSize.Returns(viewSize);
view.Handler.Returns(handler);
return view;
}

Просмотреть файл

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Microsoft.Maui;
using Microsoft.Maui.Layouts;
using NSubstitute;
using Xunit;
@ -81,32 +80,5 @@ namespace Microsoft.Maui.UnitTests.Layouts
var measurement = manager.Measure(100, double.PositiveInfinity);
Assert.Equal(expectedHeight, measurement.Height);
}
[Fact]
public void ViewsArrangedWithDesiredWidths()
{
var stack = CreateTestLayout();
var manager = new VerticalStackLayoutManager(stack);
var view1 = LayoutTestHelpers.CreateTestView(new Size(200, 100));
var view2 = LayoutTestHelpers.CreateTestView(new Size(150, 100));
var children = new List<IView>() { view1, view2 }.AsReadOnly();
stack.Children.Returns(children);
var measurement = manager.Measure(double.PositiveInfinity, double.PositiveInfinity);
manager.ArrangeChildren(new Rectangle(Point.Zero, measurement));
// The widest IView is 200, so the stack should be that wide
Assert.Equal(200, measurement.Width);
// We expect the first IView to be at 0,0 with a width of 200 and a height of 100
var expectedRectangle1 = new Rectangle(0, 0, 200, 100);
view1.Received().Arrange(Arg.Is(expectedRectangle1));
// We expect the second IView to be at 0, 100 with a width of 150 and a height of 100
var expectedRectangle2 = new Rectangle(0, 100, 150, 100);
view2.Received().Arrange(Arg.Is(expectedRectangle2));
}
}
}

Просмотреть файл

@ -1,5 +1,5 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Primitives;
namespace Microsoft.Maui.Tests
{
@ -31,6 +31,10 @@ namespace Microsoft.Maui.Tests
public FlowDirection FlowDirection => throw new NotImplementedException();
public LayoutAlignment HorizontalLayoutAlignment { get; set; }
public LayoutAlignment VerticalLayoutAlignment { get; set; }
public void Arrange(Rectangle bounds)
{
throw new NotImplementedException();