Skip to content

Commit 8db201c

Browse files
Dhivya-SF4094bot
authored andcommitted
[Windows] Fixed BoxView improper rendering inside Border (#28465)
### Issue Details: Placing a BoxView inside a Border results in incorrect sizing. ### Root Cause On Windows, when content inside a Border has the same or larger explicit size than the Border itself, the WinUI AdjustForExplicitSize logic expands the content's measured size back to its requested dimensions, even after the available size has been reduced to account for the border stroke. This causes MeasureContent to return a desired size larger than the Border by StrokeThickness * 2, resulting in the parent layout allocating an oversized layout slot. Consequently, the right and bottom border strokes can be clipped during rendering. ### Description of Change Override MeasureOverride in the Windows ContentPanel to constrain the reported desired size to the Border's explicit Width and Height when both values are set. This ensures the correct desired size is returned to the parent layout, allowing the Border to receive the intended layout slot size while preserving the existing ArrangeOverride and clipping behavior. ### Validated the behaviour in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed: Fixes #19668 ### Screenshots | Before | After | |---------|--------| | <img src="https://github.com/user-attachments/assets/b81f0982-e3c2-40d9-ab8b-0b1d743fa99a"> | <img src="https://github.com/user-attachments/assets/f45e3ebf-e48c-48dd-9c39-42c5413c9268"> |
1 parent 30d21ca commit 8db201c

5 files changed

Lines changed: 127 additions & 4 deletions

File tree

src/Controls/tests/DeviceTests/Elements/Border/BorderTests.Windows.cs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
using System.ComponentModel;
1+
using System;
2+
using System.ComponentModel;
23
using System.Threading.Tasks;
34
using Microsoft.Maui.Controls;
5+
using Microsoft.Maui.Controls.Handlers;
46
using Microsoft.Maui.Controls.Shapes;
57
using Microsoft.Maui.Graphics;
68
using Microsoft.Maui.Handlers;
9+
using Microsoft.Maui.Hosting;
710
using Microsoft.Maui.Platform;
811
using Microsoft.UI.Composition;
912
using Microsoft.UI.Xaml.Automation.Peers;
@@ -79,6 +82,89 @@ await InvokeOnMainThreadAsync(() =>
7982
});
8083
}
8184

85+
[Fact(DisplayName = "Border should not expand beyond its requested size when BoxView content is larger - Issue 19668")]
86+
public async Task BorderShouldNotExpandBeyondRequestedSizeWithBoxViewContent()
87+
{
88+
EnsureHandlerCreated(builder =>
89+
{
90+
builder.ConfigureMauiHandlers(handlers =>
91+
{
92+
handlers.AddHandler<Border, BorderHandler>();
93+
handlers.AddHandler<BoxView, BoxViewHandler>();
94+
});
95+
});
96+
97+
// BoxView is intentionally LARGER than the Border's requested size.
98+
// The bug: the BoxView pushes the Border to expand beyond its WidthRequest/HeightRequest.
99+
// The fix: the Border must constrain itself to its requested size regardless of content.
100+
var boxView = new BoxView
101+
{
102+
Color = Colors.Red,
103+
WidthRequest = 120,
104+
HeightRequest = 120,
105+
};
106+
107+
var border = new Border
108+
{
109+
BackgroundColor = Colors.Blue,
110+
WidthRequest = 80,
111+
HeightRequest = 80,
112+
Content = boxView
113+
};
114+
115+
// GetRawBitmap dimensions reflect the actual rendered size of the Border.
116+
// With bug: Border expands to fit BoxView → bitmap is ~120x120 → assertions FAIL.
117+
// After fix: Border stays at requested size → bitmap is 80x80 → assertions PASS.
118+
var bitmap = await GetRawBitmap(border, typeof(BorderHandler)).WaitAsync(TimeSpan.FromSeconds(5));
119+
120+
Assert.Equal(80, bitmap.Width, 2d);
121+
Assert.Equal(80, bitmap.Height, 2d);
122+
}
123+
124+
[Fact(DisplayName = "Border with stroke should not inflate measured size when Label content has same explicit dimensions - Issue 19668")]
125+
public async Task BorderWithStrokeShouldNotInflateMeasuredSizeWhenLabelHasSameExplicitDimensions()
126+
{
127+
EnsureHandlerCreated(builder =>
128+
{
129+
builder.ConfigureMauiHandlers(handlers =>
130+
{
131+
handlers.AddHandler<Border, BorderHandler>();
132+
handlers.AddHandler<Label, LabelHandler>();
133+
});
134+
});
135+
136+
// The root cause of #19668: when content has the SAME WidthRequest/HeightRequest as the
137+
// Border, AdjustForExplicitSize re-expands the content's measured size back to its
138+
// explicit request even after the stroke inset has reduced the available constraint.
139+
// This inflates MeasureContent's result by StrokeThickness*2, causing the parent to
140+
// allocate an oversized slot so the border's right/bottom strokes get clipped.
141+
const double requestedSize = 100;
142+
const double strokeThickness = 4;
143+
144+
var label = new Label
145+
{
146+
Text = "Hello",
147+
WidthRequest = requestedSize,
148+
HeightRequest = requestedSize,
149+
};
150+
151+
var border = new Border
152+
{
153+
BackgroundColor = Colors.Blue,
154+
WidthRequest = requestedSize,
155+
HeightRequest = requestedSize,
156+
StrokeThickness = strokeThickness,
157+
Content = label
158+
};
159+
160+
// With bug: desired size = requestedSize + StrokeThickness*2 → bitmap is ~108x108.
161+
// After fix: desired size capped at requestedSize → bitmap is 100x100.
162+
var bitmap = await GetRawBitmap(border, typeof(BorderHandler)).WaitAsync(TimeSpan.FromSeconds(5));
163+
164+
Assert.Equal(requestedSize, bitmap.Width, 2d);
165+
Assert.Equal(requestedSize, bitmap.Height, 2d);
166+
}
167+
82168
ContentPanel GetNativeBorder(BorderHandler borderHandler) =>
83169
borderHandler.PlatformView;
84170

1.05 KB
Loading
932 Bytes
Loading

src/Core/src/Platform/Windows/ContentPanel.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
#else
99
using Microsoft.Maui.Graphics.Platform;
1010
#endif
11+
using Microsoft.Maui.Primitives;
1112
using Microsoft.UI.Composition;
1213
using Microsoft.UI.Xaml;
1314
using Microsoft.UI.Xaml.Automation;
1415
using Microsoft.UI.Xaml.Automation.Peers;
16+
using Microsoft.UI.Xaml.Controls.Primitives;
1517
using Microsoft.UI.Xaml.Hosting;
1618
using Microsoft.UI.Xaml.Shapes;
1719
using Microsoft.UI.Xaml.Media;
@@ -55,6 +57,37 @@ internal FrameworkElement? Content
5557

5658
internal bool IsInnerPath { get; private set; }
5759

60+
protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize)
61+
{
62+
var measured = base.MeasureOverride(availableSize);
63+
64+
// On Windows, when content inside a Border has the same WidthRequest/HeightRequest
65+
// as the Border itself, AdjustForExplicitSize expands the content's measured size
66+
// back to its explicit request even after the stroke inset reduces the constraint.
67+
// This inflates MeasureContent's result by StrokeThickness*2, so the parent
68+
// allocates an oversized layout slot and the border renders with its right/bottom
69+
// strokes clipped. Capping here at the Border's explicit dimensions corrects the
70+
// reported desired size and ensures the parent allocates the right amount.
71+
if (_borderStroke is not null && Content is not null &&
72+
CrossPlatformLayout is IBorderView borderView)
73+
{
74+
var explicitWidth = borderView.Width;
75+
var explicitHeight = borderView.Height;
76+
77+
if (Dimension.IsExplicitSet(explicitWidth))
78+
{
79+
measured.Width = Math.Min(measured.Width, explicitWidth);
80+
}
81+
82+
if (Dimension.IsExplicitSet(explicitHeight))
83+
{
84+
measured.Height = Math.Min(measured.Height, explicitHeight);
85+
}
86+
}
87+
88+
return measured;
89+
}
90+
5891
protected override global::Windows.Foundation.Size ArrangeOverride(global::Windows.Foundation.Size finalSize)
5992
{
6093
var actual = base.ArrangeOverride(finalSize);
@@ -66,7 +99,7 @@ internal FrameworkElement? Content
6699
// We need to update the clip since the content's position might have changed
67100
UpdateClip(_borderStroke?.Shape, size.Width, size.Height);
68101

69-
return size;
102+
return actual;
70103
}
71104

72105
protected override AutomationPeer OnCreateAutomationPeer()
@@ -251,8 +284,11 @@ void UpdateClip(IShape? borderShape, double width, double height)
251284
var pathGeometry = compositor.CreatePathGeometry(path);
252285
var geometricClip = compositor.CreateGeometricClip(pathGeometry);
253286

254-
// The clip needs to consider the content's offset in case it is in a different position because of a different alignment.
255-
geometricClip.Offset = new Vector2(strokeThickness - Content.ActualOffset.X, strokeThickness - Content.ActualOffset.Y);
287+
// The clip needs to consider the content's layout slot position, using GetLayoutSlot for
288+
// a synchronous arrange-time value rather than the composition ActualOffset which can
289+
// lag behind and produce an incorrect offset on the first layout pass.
290+
var layoutSlot = LayoutInformation.GetLayoutSlot(Content);
291+
geometricClip.Offset = new Vector2(strokeThickness - (float)layoutSlot.X, strokeThickness - (float)layoutSlot.Y);
256292

257293
visual.Clip = geometricClip;
258294
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
22
override Microsoft.Maui.Platform.LayoutPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!
33
override Microsoft.Maui.Platform.MauiPasswordTextBox.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!
4+
override Microsoft.Maui.Platform.ContentPanel.MeasureOverride(Windows.Foundation.Size availableSize) -> Windows.Foundation.Size
45
override Microsoft.Maui.Platform.ContentPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!

0 commit comments

Comments
 (0)