Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
28 changes: 20 additions & 8 deletions src/Controls/src/Core/Shapes/Shape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ public double StrokeThickness
get { return (double)GetValue(StrokeThicknessProperty); }
}

/// <summary>
/// Returns the effective stroke thickness: <see cref="StrokeThickness"/> when a <see cref="Stroke"/> is set, otherwise 0.
/// </summary>
internal double EffectiveStrokeThickness => Stroke is not null ? StrokeThickness : 0;

/// <summary>
/// Gets or sets the collection of values that specify the pattern of dashes and gaps in the shape's outline. This is a bindable property.
/// </summary>
Expand Down Expand Up @@ -317,10 +322,17 @@ internal void TransformPathForBounds(PathF path, Graphics.Rect viewBounds)
// since default GetBoundsByFlattening(0.001) returns incorrect results for curves
RectF pathBounds = path.GetBoundsByFlattening(1);

viewBounds.X += StrokeThickness / 2;
viewBounds.Y += StrokeThickness / 2;
viewBounds.Width -= StrokeThickness;
viewBounds.Height -= StrokeThickness;
// Only apply stroke inset if there is an actual stroke.
// For shapes with no stroke shrinking the bounds by StrokeThickness was
// effectively collapsing very small heights into a barely visible line.
var strokeThickness = EffectiveStrokeThickness;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Logic and Correctness - This only skips the second stroke inset in TransformPathForBounds, but Rectangle.GetPath() and Ellipse.GetPath() are called before this and still subtract the raw default StrokeThickness even when Stroke is null. For the issue scenario (Rectangle with HeightRequest = 1.2, fill only), GetPath() still builds a path with y = 0.5 and h = 0.2, so the shape can still render as the thin line from #31330. The effective stroke thickness needs to be used when constructing those shape paths too.

if (strokeThickness > 0)
{
viewBounds.X += strokeThickness / 2;
viewBounds.Y += strokeThickness / 2;
viewBounds.Width -= strokeThickness;
viewBounds.Height -= strokeThickness;
}

Matrix3x2 transform;

Expand Down Expand Up @@ -456,8 +468,8 @@ protected override Size MeasureOverride(double widthConstraint, double heightCon
result.Height = boundsByFlattening.Height;
result.Width = boundsByFlattening.Width;

widthConstraint -= StrokeThickness;
heightConstraint -= StrokeThickness;
widthConstraint -= EffectiveStrokeThickness;
heightConstraint -= EffectiveStrokeThickness;

double scaleX = widthConstraint / result.Width;
double scaleY = heightConstraint / result.Height;
Expand Down Expand Up @@ -505,8 +517,8 @@ protected override Size MeasureOverride(double widthConstraint, double heightCon
break;
}

result.Height += StrokeThickness;
result.Width += StrokeThickness;
result.Height += EffectiveStrokeThickness;
result.Width += EffectiveStrokeThickness;
return result;
}

Expand Down
32 changes: 22 additions & 10 deletions src/Controls/tests/DeviceTests/Elements/Border/BorderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,34 @@ public async Task RoundedRectangleBorderLayoutIsCorrect()
var colors = new Color[16];
int index = 0;

// To calculate the x and y offsets (from the center) for a 45-45-90 triangle, we can use the radius as the hypotenuse
// which means that the x and y offsets would be radius / sqrt(2).
// To calculate the x and y offsets (from the corner) for the 45° point on a circular arc,
// use the formula: offset = radius - radius / sqrt(2).
var xy = radius - (radius / Math.Sqrt(2));

// The inner stroke edge follows a concentric arc with radius (radius - strokeThickness).
// At 45°, the inner edge offset from the corner is: radius - (radius - strokeThickness) / sqrt(2).
// Note: this is NOT outerXY + strokeThickness, because the stroke width is measured radially
// (perpendicular to the arc), not linearly along both X and Y axes.
var innerXY = radius - ((radius - strokeThickness) / Math.Sqrt(2));

for (int i = 0; i < corners.Length; i++)
{
int xdir = i == 0 || i == 2 ? 1 : -1;
int ydir = i == 0 || i == 1 ? 1 : -1;

// This marks the outside edge of the rounded corner.
// This marks the outside edge of the rounded corner (path line at 45°).
var outerX = corners[i].X + (xdir * xy);
var outerY = corners[i].Y + (ydir * xy);

// Add stroke thickness to find the inner edge of the rounded corner.
var innerX = outerX + (xdir * strokeThickness);
var innerY = outerY + (ydir * strokeThickness);
// Inner edge of the stroke at 45° (concentric arc).
var innerX = corners[i].X + (xdir * innerXY);
var innerY = corners[i].Y + (ydir * innerXY);

// Verify that the color outside of the rounded corner is the parent's color (White)
points[index] = new Point(outerX - (xdir * 0.25), outerY - (ydir * 0.25));
// Verify that the color outside of the rounded corner is the parent's color (White).
// Use 1.5dp margin (not 0.25) to stay clear of antialiasing at the stroke edge:
// at density=2 the stroke outer edge is at ~11.7px, and 0.25dp gives only ~0.7px
// clearance — inside the 1px antialiasing blend zone, causing intermittent failures.
points[index] = new Point(outerX - (xdir * 1.5), outerY - (ydir * 1.5));
colors[index] = Colors.White;
index++;

Expand All @@ -173,8 +182,11 @@ public async Task RoundedRectangleBorderLayoutIsCorrect()
colors[index] = stroke;
index++;

// Verify that the background color starts where we'd expect it to start
points[index] = new Point(innerX + (xdir * 0.25), innerY + (ydir * 0.25));
// Verify that the background color starts where we'd expect it to start.
// Use 1.5dp margin to stay clear of antialiasing at the inner stroke edge:
// on iOS @1x context (1pt=1px), innerX+0.25 gives only ~0.14px clearance from the
// 9.858pt inner edge — inside the antialiasing blend zone (near-zero tolerance on iOS).
points[index] = new Point(innerX + (xdir * 1.5), innerY + (ydir * 1.5));
colors[index] = border.BackgroundColor;
index++;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue31330.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Layouts;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 31330, "Rectangle renders as thin line instead of filled shape for small height values", PlatformAffected.Android | PlatformAffected.iOS)]
public class Issue31330 : ContentPage
{
public Issue31330()
{
var scrollView = new ScrollView();
var grid = new Grid
{
BackgroundColor = Colors.LightGray,
RowSpacing = 10,
};

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });

// Instructions
var instructions = new Label
{
Text = "Test passes if:\n1. Green BoxView (height 1.2) is visible as a filled rectangle\n2. Blue Rectangle (height 1.2) is visible as a filled rectangle (not a thin line)\n3. Both should have similar appearance",
FontAttributes = FontAttributes.Bold,
AutomationId = "Instructions"
};
Grid.SetRow(instructions, 0);
grid.Children.Add(instructions);

// BoxView with small height (reference for correct rendering)
var boxViewLabel = new Label { Text = "BoxView (height 1.2):" };
Grid.SetRow(boxViewLabel, 1);
grid.Children.Add(boxViewLabel);

var boxView = new BoxView
{
Color = Colors.Green,
WidthRequest = 50,
HeightRequest = 1.2,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.Start,
AutomationId = "TestBoxView"
};
Grid.SetRow(boxView, 1);
grid.Children.Add(boxView);

// Rectangle with small height (should render like BoxView, not as a line)
var rectangleLabel = new Label { Text = "Rectangle (height 1.2, Fill only, no Stroke):" };
Grid.SetRow(rectangleLabel, 2);
grid.Children.Add(rectangleLabel);

var rectangle = new Rectangle
{
WidthRequest = 50,
HeightRequest = 1.2,
Fill = Colors.Blue,
Stroke = null, // Explicitly no stroke
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.Start,
AutomationId = "TestRectangle"
};
Grid.SetRow(rectangle, 2);
grid.Children.Add(rectangle);

// AbsoluteLayout test (from original issue report)
var absoluteLayout = new AbsoluteLayout();
Grid.SetRow(absoluteLayout, 3);
grid.Children.Add(absoluteLayout);

double shapeWidth = 20;
double shapeHeight = 1.2;
double shapeY = 10;

// BoxView in AbsoluteLayout (reference)
var absBoxView = new BoxView
{
BackgroundColor = Colors.Green
};
AbsoluteLayout.SetLayoutBounds(absBoxView, new Rect(
0,
shapeY,
shapeWidth,
shapeHeight
));
AbsoluteLayout.SetLayoutFlags(absBoxView, AbsoluteLayoutFlags.None);
absoluteLayout.Children.Add(absBoxView);

// Rectangle in AbsoluteLayout (should match BoxView appearance)
var absRectangle = new Rectangle
{
Fill = Colors.Blue
};
AbsoluteLayout.SetLayoutBounds(absRectangle, new Rect(
30,
shapeY,
shapeWidth,
shapeHeight
));
AbsoluteLayout.SetLayoutFlags(absRectangle, AbsoluteLayoutFlags.None);
absoluteLayout.Children.Add(absRectangle);

scrollView.Content = grid;
Content = scrollView;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue31330 : _IssuesUITest
{
public Issue31330(TestDevice testDevice) : base(testDevice)
{
}
public override string Issue => "Rectangle renders as thin line instead of filled shape for small height values";

[Test]
[Category(UITestCategories.Shape)]
public void UpdateSizeOnlyWhenStrokeExists()
{
App.WaitForElement("TestBoxView");
VerifyScreenshot();
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
}
}
Loading
Loading