Skip to content

Commit bf4be4f

Browse files
CopilotPureWeen
andcommitted
Restructure ISafeAreaView2 implementation per review feedback
Co-authored-by: PureWeen <[email protected]>
1 parent a053e24 commit bf4be4f

File tree

6 files changed

+93
-62
lines changed

6 files changed

+93
-62
lines changed

src/Controls/src/Core/ContentView/ContentView.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Maui.Controls
99
/// <include file="../../docs/Microsoft.Maui.Controls/ContentView.xml" path="Type[@FullName='Microsoft.Maui.Controls.ContentView']/Docs/*" />
1010
[ContentProperty("Content")]
1111
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
12-
public partial class ContentView : TemplatedView, IContentView
12+
public partial class ContentView : TemplatedView, IContentView, ISafeAreaView2
1313
{
1414
/// <summary>Bindable property for <see cref="Content"/>.</summary>
1515
public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(ContentView), null, propertyChanged: TemplateUtilities.OnContentChanged);
@@ -55,5 +55,18 @@ private protected override string GetDebuggerDisplay()
5555
var contentText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content);
5656
return $"{base.GetDebuggerDisplay()}, {contentText}";
5757
}
58+
59+
#region ISafeAreaView2
60+
61+
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
62+
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for content views
63+
64+
/// <inheritdoc cref="ISafeAreaView2.IgnoreSafeAreaForEdge"/>
65+
bool ISafeAreaView2.IgnoreSafeAreaForEdge(int edge)
66+
{
67+
return SafeAreaGuides.ShouldIgnoreSafeAreaForEdge(this, edge);
68+
}
69+
70+
#endregion
5871
}
5972
}

src/Controls/src/Core/Layout/Layout.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Microsoft.Maui.Controls
1616
/// </summary>
1717
[ContentProperty(nameof(Children))]
1818
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
19-
public abstract partial class Layout : View, Maui.ILayout, IList<IView>, IBindableLayout, IPaddingElement, IVisualTreeElement, ISafeAreaView, IInputTransparentContainerElement
19+
public abstract partial class Layout : View, Maui.ILayout, IList<IView>, IBindableLayout, IPaddingElement, IVisualTreeElement, ISafeAreaView, IInputTransparentContainerElement, ISafeAreaView2
2020
{
2121
protected ILayoutManager _layoutManager;
2222

@@ -394,5 +394,18 @@ private protected override string GetDebuggerDisplay()
394394
{
395395
return $"{base.GetDebuggerDisplay()}, ChildCount = {Count}";
396396
}
397+
398+
#region ISafeAreaView2
399+
400+
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
401+
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for layouts
402+
403+
/// <inheritdoc cref="ISafeAreaView2.IgnoreSafeAreaForEdge"/>
404+
bool ISafeAreaView2.IgnoreSafeAreaForEdge(int edge)
405+
{
406+
return SafeAreaGuides.ShouldIgnoreSafeAreaForEdge(this, edge);
407+
}
408+
409+
#endregion
397410
}
398411
}

src/Controls/src/Core/Page/Page.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,7 @@ Thickness ISafeAreaView2.SafeAreaInsets
266266
/// <inheritdoc cref="ISafeAreaView2.IgnoreSafeAreaForEdge"/>
267267
bool ISafeAreaView2.IgnoreSafeAreaForEdge(int edge)
268268
{
269-
// Page doesn't use the SafeAreaGuides attached property, so always fall back to legacy behavior
270-
return ((ISafeAreaView)this).IgnoreSafeArea;
269+
return SafeAreaGuides.ShouldIgnoreSafeAreaForEdge(this, edge);
271270
}
272271

273272
/// <summary>

src/Controls/src/Core/SafeAreaGuides.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,46 @@ public static SafeAreaGroup GetIgnoreSafeAreaForEdge(BindableObject bindable, in
8080
_ => SafeAreaGroup.None
8181
};
8282
}
83+
84+
/// <summary>
85+
/// Gets the effective safe area behavior for a specific edge for elements that implement ISafeAreaView2.
86+
/// This method handles the logic for checking attached properties and falling back to legacy behavior.
87+
/// </summary>
88+
/// <param name="bindable">The bindable object that implements ISafeAreaView2.</param>
89+
/// <param name="edge">The edge to get the behavior for (0=Left, 1=Top, 2=Right, 3=Bottom).</param>
90+
/// <returns>True if safe area should be ignored for this edge, false otherwise.</returns>
91+
internal static bool ShouldIgnoreSafeAreaForEdge(BindableObject bindable, int edge)
92+
{
93+
// Check if SafeAreaGuides attached property has been explicitly set
94+
var safeAreaGuides = GetIgnoreSafeArea(bindable);
95+
var defaultValue = (SafeAreaGroup[])IgnoreSafeAreaProperty.DefaultValue;
96+
97+
// Only use attached property if it's different from default (meaning it was explicitly set)
98+
if (safeAreaGuides != null && !ReferenceEquals(safeAreaGuides, defaultValue) &&
99+
(safeAreaGuides.Length != defaultValue.Length || !AreArraysEqual(safeAreaGuides, defaultValue)))
100+
{
101+
var groupForEdge = GetIgnoreSafeAreaForEdge(bindable, edge);
102+
return groupForEdge.HasFlag(SafeAreaGroup.All);
103+
}
104+
105+
// Fall back to legacy behavior if available
106+
if (bindable is ISafeAreaView legacySafeAreaView)
107+
{
108+
return legacySafeAreaView.IgnoreSafeArea;
109+
}
110+
111+
// Default to false (respect safe area)
112+
return false;
113+
}
114+
115+
private static bool AreArraysEqual(SafeAreaGroup[] arr1, SafeAreaGroup[] arr2)
116+
{
117+
if (arr1.Length != arr2.Length) return false;
118+
for (int i = 0; i < arr1.Length; i++)
119+
{
120+
if (arr1[i] != arr2[i]) return false;
121+
}
122+
return true;
123+
}
83124
}
84125
}

src/Controls/src/Core/View/View.cs

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.Maui.Controls
1717
/// This is the base class for <see cref="Layout"/> and most of the controls.
1818
/// Because <see cref="View" /> ultimately inherits from <see cref="BindableObject" />, application developers can use the Model-View-ViewModel architecture, as well as XAML, to develop portable user interfaces.
1919
/// </remarks>
20-
public partial class View : VisualElement, IViewController, IGestureController, IGestureRecognizers, IView, IPropertyMapperView, IHotReloadableView, IControlsView, ISafeAreaView2
20+
public partial class View : VisualElement, IViewController, IGestureController, IGestureRecognizers, IView, IPropertyMapperView, IHotReloadableView, IControlsView
2121
{
2222
protected internal IGestureController GestureController => this;
2323

@@ -335,48 +335,6 @@ void IHotReloadableView.Reload()
335335

336336
#endregion
337337

338-
#region ISafeAreaView2
339-
340-
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
341-
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for views
342-
343-
/// <inheritdoc cref="ISafeAreaView2.IgnoreSafeAreaForEdge"/>
344-
bool ISafeAreaView2.IgnoreSafeAreaForEdge(int edge)
345-
{
346-
// Check if SafeAreaGuides attached property has been explicitly set
347-
var safeAreaGuides = SafeAreaGuides.GetIgnoreSafeArea(this);
348-
var defaultValue = (SafeAreaGroup[])SafeAreaGuides.IgnoreSafeAreaProperty.DefaultValue;
349-
350-
// Only use attached property if it's different from default (meaning it was explicitly set)
351-
if (safeAreaGuides != null && !ReferenceEquals(safeAreaGuides, defaultValue) &&
352-
(safeAreaGuides.Length != defaultValue.Length || !AreArraysEqual(safeAreaGuides, defaultValue)))
353-
{
354-
var groupForEdge = SafeAreaGuides.GetIgnoreSafeAreaForEdge(this, edge);
355-
return groupForEdge.HasFlag(SafeAreaGroup.All);
356-
}
357-
358-
// Fall back to legacy behavior if available
359-
if (this is ISafeAreaView legacySafeAreaView)
360-
{
361-
return legacySafeAreaView.IgnoreSafeArea;
362-
}
363-
364-
// Default to false (respect safe area)
365-
return false;
366-
}
367-
368-
private static bool AreArraysEqual(SafeAreaGroup[] arr1, SafeAreaGroup[] arr2)
369-
{
370-
if (arr1.Length != arr2.Length) return false;
371-
for (int i = 0; i < arr1.Length; i++)
372-
{
373-
if (arr1[i] != arr2[i]) return false;
374-
}
375-
return true;
376-
}
377-
378-
#endregion
379-
380338
#nullable disable
381339
}
382340
}

src/Controls/tests/Core.UnitTests/SafeAreaGuidesTests.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,29 @@ public void Layout_IgnoreSafeAreaForEdge_FallsBackToLegacyProperty()
140140
}
141141

142142
[Fact]
143-
public void View_ImplementsISafeAreaView2()
143+
public void Page_ImplementsISafeAreaView2()
144144
{
145-
var label = new Label();
145+
var page = new ContentPage();
146146

147-
Assert.IsAssignableFrom<ISafeAreaView2>(label);
147+
Assert.IsAssignableFrom<ISafeAreaView2>(page);
148148
}
149149

150150
[Fact]
151-
public void View_IgnoreSafeAreaForEdge_UsesAttachedProperty()
151+
public void ContentView_ImplementsISafeAreaView2()
152152
{
153-
var label = new Label();
154-
SafeAreaGuides.SetIgnoreSafeArea(label, new[] { SafeAreaGroup.All, SafeAreaGroup.None, SafeAreaGroup.All, SafeAreaGroup.None });
153+
var contentView = new ContentView();
154+
155+
Assert.IsAssignableFrom<ISafeAreaView2>(contentView);
156+
}
157+
158+
[Fact]
159+
public void Page_IgnoreSafeAreaForEdge_UsesAttachedProperty()
160+
{
161+
var page = new ContentPage();
162+
SafeAreaGuides.SetIgnoreSafeArea(page, new[] { SafeAreaGroup.All, SafeAreaGroup.None, SafeAreaGroup.All, SafeAreaGroup.None });
155163

156164
// Test via ISafeAreaView2 interface
157-
var safeAreaView2 = (ISafeAreaView2)label;
165+
var safeAreaView2 = (ISafeAreaView2)page;
158166

159167
Assert.True(safeAreaView2.IgnoreSafeAreaForEdge(0)); // Left = All
160168
Assert.False(safeAreaView2.IgnoreSafeAreaForEdge(1)); // Top = None
@@ -163,12 +171,12 @@ public void View_IgnoreSafeAreaForEdge_UsesAttachedProperty()
163171
}
164172

165173
[Fact]
166-
public void View_IgnoreSafeAreaForEdge_FallsBackToDefaultWhenNoLegacySupport()
174+
public void ContentView_IgnoreSafeAreaForEdge_FallsBackToDefaultWhenNoLegacySupport()
167175
{
168-
var label = new Label(); // Label doesn't implement ISafeAreaView
176+
var contentView = new ContentView(); // ContentView doesn't implement ISafeAreaView
169177

170178
// Should default to false when no attached property is set and no legacy support
171-
var safeAreaView2 = (ISafeAreaView2)label;
179+
var safeAreaView2 = (ISafeAreaView2)contentView;
172180

173181
Assert.False(safeAreaView2.IgnoreSafeAreaForEdge(0));
174182
Assert.False(safeAreaView2.IgnoreSafeAreaForEdge(1));
@@ -181,17 +189,16 @@ public void View_IgnoreSafeAreaForEdge_FallsBackToDefaultWhenNoLegacySupport()
181189
public void SafeAreaGuides_CanReplaceUseSafeAreaScenario()
182190
{
183191
// This test mimics the usage pattern from Issue3809 and ShellTests.iOS
184-
var contentPage = new ContentPage()
192+
var contentView = new ContentView()
185193
{
186-
Content = new Label() { Text = "Test Page" },
187-
Padding = new Thickness(25, 25, 25, 25)
194+
Content = new Label() { Text = "Test Page" }
188195
};
189196

190197
// Legacy approach: contentPage.On<iOS>().SetUseSafeArea(true);
191198
// New approach: use SafeAreaGuides attached property
192-
SafeAreaGuides.SetIgnoreSafeArea(contentPage.Content, new[] { SafeAreaGroup.None }); // Respect all safe areas
199+
SafeAreaGuides.SetIgnoreSafeArea(contentView, new[] { SafeAreaGroup.None }); // Respect all safe areas
193200

194-
var safeAreaView2 = (ISafeAreaView2)contentPage.Content;
201+
var safeAreaView2 = (ISafeAreaView2)contentView;
195202

196203
// All edges should respect safe area (false)
197204
Assert.False(safeAreaView2.IgnoreSafeAreaForEdge(0));

0 commit comments

Comments
 (0)