Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8e5fe27
Initial plan
Copilot Jun 30, 2025
50b9fd0
Add Edge Support for SafeAreas and extend to ScrollView
PureWeen Jul 24, 2025
9d3e947
- fix public APIS
PureWeen Jul 24, 2025
ea1d701
- fix publicapi
PureWeen Jul 24, 2025
7568cba
- fix default safearea
PureWeen Jul 24, 2025
dfbf462
- fix tests
PureWeen Jul 24, 2025
221807a
- publicapi fix
PureWeen Jul 25, 2025
7d0cc5b
Fix Controls.Sample compilation errors - update SafeAreaElement API u…
Copilot Jul 25, 2025
c408853
Remove IgnoreSafeAreaForEdge from ISafeAreaView2 interface and conver…
Copilot Jul 25, 2025
40b81f7
- fix
PureWeen Jul 25, 2025
12ee04a
- fix netstandard
PureWeen Jul 25, 2025
9de66b9
- fix
PureWeen Jul 25, 2025
f1ec5ef
Make SafeAreaEdges immutable and update SafeAreaRegions flag values
Copilot Jul 25, 2025
48f80fb
Add *.bak to .gitignore to exclude backup files
Copilot Jul 25, 2025
ea762aa
Remove formatting-only changes unrelated to SafeArea functionality
Copilot Jul 25, 2025
5df550a
Add comprehensive diagnostic logging and timing analysis for build pe…
Copilot Jul 25, 2025
63c3b7b
Revert diagnostic logging changes from Cake file
Copilot Jul 25, 2025
f21f92d
Simplify Issue28986 test to use buttons only instead of pickers
Copilot Jul 25, 2025
f5058c8
Make SafeAreaEdges a readonly struct
Copilot Jul 25, 2025
97dbe78
- fix test
PureWeen Jul 25, 2025
5fae900
Fix SafeArea UI tests to validate actual screen positioning instead o…
Copilot Jul 25, 2025
5c72da1
- fix up defaults
PureWeen Jul 25, 2025
f77577e
- fix edge case
PureWeen Jul 26, 2025
da577bd
- fix tests and defaults
PureWeen Jul 26, 2025
ce8d2e0
- fix tests
PureWeen Jul 26, 2025
1d913bb
- fix test
PureWeen Jul 26, 2025
76817da
- slight adjustment to contentsize
PureWeen Jul 26, 2025
a9b3b7f
- maybe?
PureWeen Jul 26, 2025
8a7e376
- cleanup code
PureWeen Jul 26, 2025
8778141
- fix test and add a comment
PureWeen Jul 26, 2025
26b5d87
- fix device test and remove some apis
PureWeen Jul 27, 2025
765fffa
Add per-edge SafeArea validation, ScrollView test suite, and revert .…
Copilot Jul 27, 2025
d282b5c
Add Entry control for SoftInput testing and fix ScrollView SafeAreaEdges
Copilot Jul 27, 2025
c0af54e
- fix UI Tests to validate more scenarios
PureWeen Jul 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.nuspec/
.buildtasks/
templatesTest/
*.bak

# User-specific files
*.rsuser
Expand Down
12 changes: 6 additions & 6 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,15 @@
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="local" value="LOCAL_PLACEHOLDER" />
<add key="nuget-only" value="NUGET_ONLY_PLACEHOLDER" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" protocolVersion="3" />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" protocolVersion="3" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" protocolVersion="3" />
<add key="benchmark-dotnet-prerelease" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/benchmark-dotnet-prerelease/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
<add key="dotnet8-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet9-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-transport/nuget/v3/index.json" />
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />
<add key="dotnet10-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json" />
<add key="skiasharp" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/SkiaSharp/nuget/v3/index.json" />
<add key="dotnet-libraries" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" />
<add key="dotnet-libraries-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries-transport/nuget/v3/index.json" />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" protocolVersion="3" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" protocolVersion="3" />
<!-- Added manually for .NET 8 MAUI -->
<add key="darc-pub-dotnet-maui-a33a875e" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-maui-a33a875e/nuget/v3/index.json" />
<!-- Added manually for dotnet/runtime 8.0.18 -->
Expand All @@ -35,6 +31,10 @@
<add key="darc-pub-dotnet-android-cdb777a" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-android-cdb777a0/nuget/v3/index.json" />
<!-- Added manually for .NET 9 macios -->
<add key="darc-pub-dotnet-macios-0e1a194" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-macios-0e1a194f/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
<add key="dotnet8-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet9-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-transport/nuget/v3/index.json" />
</packageSources>
<activePackageSource>
<add key="All" value="(Aggregate source)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Pages.TitleBarPage"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
ios:Page.UseSafeArea="False"
SafeAreaEdges="All"
Shell.NavBarIsVisible="True"
NavigationPage.HasNavigationBar="True"
Title="TitleBarPage">

<Grid IgnoreSafeArea="True"
<Grid SafeAreaEdges="All"
RowDefinitions="Auto,*"
ColumnDefinitions="*,*">
<VerticalStackLayout IgnoreSafeArea="True"
<VerticalStackLayout SafeAreaEdges="All"
Spacing="16"
Margin="16"
Grid.Column="0">
Expand All @@ -20,7 +19,7 @@
Text="Content Options"
FontSize="24"/>

<HorizontalStackLayout IgnoreSafeArea="True">
<HorizontalStackLayout SafeAreaEdges="All">
<CheckBox
x:Name="SetIconCheckBox"
IsChecked="False"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public LargeTitlesPageiOS()
var navPage = (Parent as NavigationPage)!;
navPage.On<iOS>().SetPrefersLargeTitles(true);
var page = new ContentPage { Title = "New Title", BackgroundColor = Colors.Red };
page.On<iOS>().SetUseSafeArea(true);
page.SafeAreaEdges = Microsoft.Maui.SafeAreaEdges.All;
var listView = new ListView(ListViewCachingStrategy.RecycleElementAndDataTemplate)
{
HasUnevenRows = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
x:Class="Maui.Controls.Sample.Pages.iOSSafeAreaPage"
Title="Safe Area"
ios:Page.UseSafeArea="True">
SafeAreaEdges="None">
<StackLayout>
<Label Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis enim redargueret? At modo dixeras nihil in istis rebus esse, quod interesset. Et quidem, inquit, vehementer errat; Semper enim ex eo, quod maximas partes continet latissimeque funditur, tota res appellatur. Equidem, sed audistine modo de Carneade? Duo Reges: constructio interrete." />
<Button Text="Disable Use Safe Area"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;

namespace Maui.Controls.Sample.Pages
{
Expand All @@ -14,7 +12,7 @@ public iOSSafeAreaPage()

void OnButtonClicked(object sender, EventArgs e)
{
On<iOS>().SetUseSafeArea(false);
this.SafeAreaEdges = Microsoft.Maui.SafeAreaEdges.None;
(sender as Button)!.IsEnabled = false;
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/Controls/src/Core.Design/SafeAreaEdgesTypeDesignConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.ComponentModel;
using Controls.Core.Design;
using Microsoft.Maui;

namespace Microsoft.Maui.Controls.Design
{
public class SafeAreaEdgesTypeDesignConverter : StringConverter
{
public override bool IsValid(ITypeDescriptorContext context, object value)
{
// MUST MATCH SafeAreaEdgesTypeConverter.ConvertFrom
string strValue = value?.ToString()?.Trim();
if (string.IsNullOrEmpty(strValue))
return false;

// Split by comma and check each part
string[] parts = strValue.Split(',');

// Must have 1, 2, or 4 parts
if (parts.Length != 1 && parts.Length != 2 && parts.Length != 4)
return false;

// Each part must be a valid SafeAreaRegions value
foreach (string part in parts)
{
string trimmedPart = part.Trim();
if (!string.Equals(trimmedPart, "All", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(trimmedPart, "None", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(trimmedPart, "Default", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(trimmedPart, "SoftInput", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(trimmedPart, "Container", StringComparison.OrdinalIgnoreCase))
{
return false;
}
}

return true;
}
}
}
38 changes: 37 additions & 1 deletion src/Controls/src/Core/Border/Border.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Microsoft.Maui.Controls
{
[ContentProperty(nameof(Content))]
public class Border : View, IContentView, IBorderView, IPaddingElement
public class Border : View, IContentView, IBorderView, IPaddingElement, ISafeAreaElement, ISafeAreaView2
{
float[]? _strokeDashPattern;

Expand All @@ -32,6 +32,9 @@ public class Border : View, IContentView, IBorderView, IPaddingElement
/// <summary>Bindable property for <see cref="Padding"/>.</summary>
public static readonly BindableProperty PaddingProperty = PaddingElement.PaddingProperty;

/// <summary>Bindable property for <see cref="SafeAreaEdges"/>.</summary>
public static readonly BindableProperty SafeAreaEdgesProperty = SafeAreaElement.SafeAreaEdgesProperty;

public View? Content
{
get { return (View?)GetValue(ContentProperty); }
Expand All @@ -44,6 +47,21 @@ public Thickness Padding
set => SetValue(PaddingElement.PaddingProperty, value);
}

/// <summary>
/// Gets or sets the safe area edges to obey for this border.
/// The default value is SafeAreaEdges.Default (None - edge to edge).
/// </summary>
/// <remarks>
/// This property controls which edges of the border should obey safe area insets.
/// Use SafeAreaRegions.None for edge-to-edge content, SafeAreaRegions.All to obey all safe area insets,
/// SafeAreaRegions.Container for content that flows under keyboard but stays out of bars/notch, or SafeAreaRegions.Keyboard for keyboard-aware behavior.
/// </remarks>
public SafeAreaEdges SafeAreaEdges
{
get => (SafeAreaEdges)GetValue(SafeAreaElement.SafeAreaEdgesProperty);
set => SetValue(SafeAreaElement.SafeAreaEdgesProperty, value);
}

/// <summary>Bindable property for <see cref="StrokeShape"/>.</summary>
public static readonly BindableProperty StrokeShapeProperty =
BindableProperty.Create(nameof(StrokeShape), typeof(IShape), typeof(Border), new Rectangle(),
Expand Down Expand Up @@ -332,5 +350,23 @@ void UpdateStrokeShape()
strokeShape.StrokeThickness = StrokeThickness;
}
}

/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
// Use direct property
var regionForEdge = SafeAreaEdges.GetEdge(edge);

// For Border, return as-is
return regionForEdge;
}

/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for borders

SafeAreaEdges ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()
{
return SafeAreaEdges.None;
}
}
}
47 changes: 46 additions & 1 deletion src/Controls/src/Core/ContentPage/ContentPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.Maui.Controls
/// <include file="../../docs/Microsoft.Maui.Controls/ContentPage.xml" path="Type[@FullName='Microsoft.Maui.Controls.ContentPage']/Docs/*" />
[ContentProperty("Content")]
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public partial class ContentPage : TemplatedPage, IContentView, HotReload.IHotReloadableView
public partial class ContentPage : TemplatedPage, IContentView, HotReload.IHotReloadableView, ISafeAreaElement, ISafeAreaView2
{
/// <summary>Bindable property for <see cref="Content"/>.</summary>
public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(ContentPage), null, propertyChanged: TemplateUtilities.OnContentChanged);
Expand All @@ -28,6 +28,9 @@ public View Content
public static readonly BindableProperty HideSoftInputOnTappedProperty
= BindableProperty.Create(nameof(HideSoftInputOnTapped), typeof(bool), typeof(ContentPage), false);

/// <summary>Bindable property for <see cref="SafeAreaEdges"/>.</summary>
public static readonly BindableProperty SafeAreaEdgesProperty = SafeAreaElement.SafeAreaEdgesProperty;

/// <summary>
/// Gets or sets a value that indicates whether tapping anywhere on the page will cause the soft input to hide.
/// </summary>
Expand All @@ -37,6 +40,21 @@ public bool HideSoftInputOnTapped
set { SetValue(HideSoftInputOnTappedProperty, value); }
}

/// <summary>
/// Gets or sets the safe area edges to obey for this content page.
/// The default value is SafeAreaEdges.Default (None - edge to edge).
/// </summary>
/// <remarks>
/// This property controls which edges of the content page should obey safe area insets.
/// Use SafeAreaRegions.None for edge-to-edge content, SafeAreaRegions.All to obey all safe area insets,
/// SafeAreaRegions.Container for content that flows under keyboard but stays out of bars/notch, or SafeAreaRegions.SoftInput for keyboard-aware behavior.
/// </remarks>
public SafeAreaEdges SafeAreaEdges
{
get => (SafeAreaEdges)GetValue(SafeAreaElement.SafeAreaEdgesProperty);
set => SetValue(SafeAreaElement.SafeAreaEdgesProperty, value);
}

public ContentPage()
{
this.NavigatedTo += (_, _) => UpdateHideSoftInputOnTapped();
Expand Down Expand Up @@ -151,6 +169,33 @@ Size IContentView.CrossPlatformMeasure(double widthConstraint, double heightCons
return (this as ICrossPlatformLayout).CrossPlatformMeasure(widthConstraint, heightConstraint);
}

/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
// Check if the developer has explicitly set SafeAreaEdges
if (IsSet(SafeAreaEdgesProperty))
{
// Developer has explicitly set SafeAreaEdges, use it directly
return SafeAreaEdges.GetEdge(edge);
}

// Developer hasn't set SafeAreaEdges, fall back to legacy IgnoreSafeArea behavior
var ignoreSafeArea = ((ISafeAreaView)this).IgnoreSafeArea;
if (ignoreSafeArea)
{
return SafeAreaRegions.None; // If legacy says "ignore", return None (edge-to-edge)
}
else
{
return SafeAreaRegions.Container; // If legacy says "don't ignore", return Container
}
}

SafeAreaEdges ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()
{
return SafeAreaEdges.None;
}

private protected override string GetDebuggerDisplay()
{
var contentText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content);
Expand Down
40 changes: 38 additions & 2 deletions src/Controls/src/Core/ContentView/ContentView.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#nullable disable
using System.Diagnostics;

using Microsoft.Maui;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;

Expand All @@ -9,7 +9,7 @@ namespace Microsoft.Maui.Controls
/// <include file="../../docs/Microsoft.Maui.Controls/ContentView.xml" path="Type[@FullName='Microsoft.Maui.Controls.ContentView']/Docs/*" />
[ContentProperty("Content")]
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public partial class ContentView : TemplatedView, IContentView
public partial class ContentView : TemplatedView, IContentView, ISafeAreaView2, ISafeAreaElement
{
/// <summary>Bindable property for <see cref="Content"/>.</summary>
public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(ContentView), null, propertyChanged: TemplateUtilities.OnContentChanged);
Expand All @@ -21,6 +21,24 @@ public View Content
set { SetValue(ContentProperty, value); }
}

/// <summary>Bindable property for <see cref="SafeAreaEdges"/>.</summary>
public static readonly BindableProperty SafeAreaEdgesProperty = SafeAreaElement.SafeAreaEdgesProperty;

/// <summary>
/// Gets or sets the safe area edges to obey for this content view.
/// The default value is SafeAreaEdges.Default (None - edge to edge).
/// </summary>
/// <remarks>
/// This property controls which edges of the content view should obey safe area insets.
/// Use SafeAreaRegions.None for edge-to-edge content, SafeAreaRegions.All to obey all safe area insets,
/// SafeAreaRegions.Container for content that flows under keyboard but stays out of bars/notch, or SafeAreaRegions.Keyboard for keyboard-aware behavior.
/// </remarks>
public SafeAreaEdges SafeAreaEdges
{
get => (SafeAreaEdges)GetValue(SafeAreaElement.SafeAreaEdgesProperty);
set => SetValue(SafeAreaElement.SafeAreaEdgesProperty, value);
}

protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
Expand Down Expand Up @@ -50,10 +68,28 @@ internal override void SetChildInheritedBindingContext(Element child, object con

IView IContentView.PresentedContent => ((this as IControlTemplated).TemplateRoot as IView) ?? Content;

SafeAreaEdges ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()
{
return SafeAreaEdges.None;
}

private protected override string GetDebuggerDisplay()
{
var contentText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content);
return $"{base.GetDebuggerDisplay()}, {contentText}";
}

/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for content views

/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
// Use direct property
var regionForEdge = SafeAreaEdges.GetEdge(edge);

// For ContentView, return the region directly
return regionForEdge;
}
}
}
Loading
Loading