Selaa lähdekoodia

Merge remote-tracking branch 'origin/master' into fixes/mac-os-constrain-window-size

# Conflicts:
#	samples/IntegrationTestApp/ShowWindowTest.axaml
Dan Walmsley 2 vuotta sitten
vanhempi
sitoutus
50b11709be
27 muutettua tiedostoa jossa 732 lisäystä ja 76 poistoa
  1. 1 1
      samples/ControlCatalog.Android/Resources/values/styles.xml
  2. 8 1
      samples/ControlCatalog.Browser/app.css
  3. 2 2
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  4. 1 5
      samples/ControlCatalog.iOS/Info.plist
  5. 2 2
      samples/ControlCatalog/App.xaml.cs
  6. 37 1
      samples/ControlCatalog/MainView.xaml.cs
  7. 0 1
      samples/ControlCatalog/MainWindow.xaml.cs
  8. 16 6
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  9. 26 4
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  10. 8 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  11. 6 6
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  12. 15 0
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  13. 6 0
      src/Android/Avalonia.Android/AvaloniaView.cs
  14. 235 0
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  15. 24 19
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  16. 55 0
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  17. 4 1
      src/Avalonia.Controls/TopLevel.cs
  18. 45 0
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  19. 11 0
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  20. 9 0
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  21. 22 0
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
  22. 3 1
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  23. 28 7
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  24. 83 0
      src/iOS/Avalonia.iOS/InsetsManager.cs
  25. 74 0
      src/iOS/Avalonia.iOS/ViewController.cs
  26. 10 10
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  27. 1 1
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

+ 1 - 1
samples/ControlCatalog.Android/Resources/values/styles.xml

@@ -4,7 +4,7 @@
   <style name="MyTheme">
   <style name="MyTheme">
   </style>
   </style>
 
 
-  <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
+  <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
     <item name="android:windowActionBar">false</item>
     <item name="android:windowActionBar">false</item>
     <item name="android:windowNoTitle">true</item>
     <item name="android:windowNoTitle">true</item>
   </style>
   </style>

+ 8 - 1
samples/ControlCatalog.Browser/app.css

@@ -1,4 +1,11 @@
-#out {
+:root {
+    --sat: env(safe-area-inset-top);
+    --sar: env(safe-area-inset-right);
+    --sab: env(safe-area-inset-bottom);
+    --sal: env(safe-area-inset-left);
+}
+
+#out {
     height: 100vh;
     height: 100vh;
     width: 100vw
     width: 100vw
 }
 }

+ 2 - 2
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@@ -3,7 +3,7 @@
     <OutputType>Exe</OutputType>
     <OutputType>Exe</OutputType>
     <ProvisioningType>manual</ProvisioningType>
     <ProvisioningType>manual</ProvisioningType>
     <TargetFramework>net6.0-ios</TargetFramework>
     <TargetFramework>net6.0-ios</TargetFramework>
-    <SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
+    <SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
     <!-- temporal workaround for our GL interface backend -->
     <!-- temporal workaround for our GL interface backend -->
     <UseInterpreter>True</UseInterpreter>
     <UseInterpreter>True</UseInterpreter>
     <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
     <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
@@ -16,4 +16,4 @@
     <ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
     <ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
   </ItemGroup>
   </ItemGroup>
-</Project>
+</Project>

+ 1 - 5
samples/ControlCatalog.iOS/Info.plist

@@ -13,7 +13,7 @@
 	<key>LSRequiresIPhoneOS</key>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
 	<true/>
 	<key>MinimumOSVersion</key>
 	<key>MinimumOSVersion</key>
-	<string>10.0</string>
+	<string>13.0</string>
 	<key>UIDeviceFamily</key>
 	<key>UIDeviceFamily</key>
 	<array>
 	<array>
 		<integer>1</integer>
 		<integer>1</integer>
@@ -39,9 +39,5 @@
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
 	</array>
-	<key>UIStatusBarHidden</key>
-	<true/>
-	<key>UIViewControllerBasedStatusBarAppearance</key>
-	<false/>
 </dict>
 </dict>
 </plist>
 </plist>

+ 2 - 2
samples/ControlCatalog/App.xaml.cs

@@ -44,11 +44,11 @@ namespace ControlCatalog
         {
         {
             if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
             if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
             {
             {
-                desktopLifetime.MainWindow = new MainWindow();
+                desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
             }
             }
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
             {
             {
-                singleViewLifetime.MainView = new MainView();
+                singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() };
             }
             }
 
 
             base.OnFrameworkInitializationCompleted();
             base.OnFrameworkInitializationCompleted();

+ 37 - 1
samples/ControlCatalog/MainView.xaml.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections;
 using System.Collections;
+using System.Threading.Tasks;
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
@@ -12,6 +13,7 @@ using Avalonia.VisualTree;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using ControlCatalog.Models;
 using ControlCatalog.Models;
 using ControlCatalog.Pages;
 using ControlCatalog.Pages;
+using ControlCatalog.ViewModels;
 
 
 namespace ControlCatalog
 namespace ControlCatalog
 {
 {
@@ -99,13 +101,47 @@ namespace ControlCatalog
             };
             };
         }
         }
 
 
+        internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
+        
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
         {
             base.OnAttachedToVisualTree(e);
             base.OnAttachedToVisualTree(e);
+
             var decorations = this.Get<ComboBox>("Decorations");
             var decorations = this.Get<ComboBox>("Decorations");
             if (VisualRoot is Window window)
             if (VisualRoot is Window window)
                 decorations.SelectedIndex = (int)window.SystemDecorations;
                 decorations.SelectedIndex = (int)window.SystemDecorations;
-            
+
+            var insets = TopLevel.GetTopLevel(this)!.InsetsManager;
+            if (insets != null)
+            {
+                // In real life application these events should be unsubscribed to avoid memory leaks.
+                ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+                insets.SafeAreaChanged += (sender, args) =>
+                {
+                    ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+                };
+
+                ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
+                ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+
+                ViewModel.PropertyChanged += async (sender, args) =>
+                {
+                    if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge))
+                    {
+                        insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge;
+                    }
+                    else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible))
+                    {
+                        insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible;
+                    }
+
+                    // Give the OS some time to apply new values and refresh the view model.
+                    await Task.Delay(100);
+                    ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
+                    ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+                };
+            }
+
             _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged;
             _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged;
             PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
             PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
         }
         }

+ 0 - 1
samples/ControlCatalog/MainWindow.xaml.cs

@@ -17,7 +17,6 @@ namespace ControlCatalog
         {
         {
             this.InitializeComponent();
             this.InitializeComponent();
 
 
-            DataContext = new MainWindowViewModel();
             _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
             _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
         }
         }
 
 

+ 16 - 6
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@@ -5,11 +5,21 @@
              xmlns:viewModels="using:ControlCatalog.ViewModels"
              xmlns:viewModels="using:ControlCatalog.ViewModels"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="ControlCatalog.Pages.WindowCustomizationsPage"
              x:Class="ControlCatalog.Pages.WindowCustomizationsPage"
-             x:DataType="viewModels:MainWindowViewModel">
-  <StackPanel Spacing="10"  Margin="25">
-    <CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
-    <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />    
-    <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
-    <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
+             x:DataType="viewModels:MainWindowViewModel"
+             x:CompileBindings="True">
+  <StackPanel>
+    <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
+      <TextBlock Classes="h2" Text="Desktop properties" Margin="4" />
+      <CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
+      <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
+      <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
+      <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
+    </StackPanel>
+    <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
+      <TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
+      <CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
+      <CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
+      <TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
+    </StackPanel>
   </StackPanel>
   </StackPanel>
 </UserControl>
 </UserControl>

+ 26 - 4
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -6,6 +6,7 @@ using Avalonia.Platform;
 using Avalonia.Reactive;
 using Avalonia.Reactive;
 using System;
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
+using Avalonia;
 using MiniMvvm;
 using MiniMvvm;
 
 
 namespace ControlCatalog.ViewModels
 namespace ControlCatalog.ViewModels
@@ -20,6 +21,9 @@ namespace ControlCatalog.ViewModels
         private bool _systemTitleBarEnabled;
         private bool _systemTitleBarEnabled;
         private bool _preferSystemChromeEnabled;
         private bool _preferSystemChromeEnabled;
         private double _titleBarHeight;
         private double _titleBarHeight;
+        private bool _isSystemBarVisible;
+        private bool _displayEdgeToEdge;
+        private Thickness _safeAreaPadding;
 
 
         public MainWindowViewModel()
         public MainWindowViewModel()
         {
         {
@@ -78,25 +82,25 @@ namespace ControlCatalog.ViewModels
         {
         {
             get { return _chromeHints; }
             get { return _chromeHints; }
             set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
             set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
-        }        
+        }
 
 
         public bool ExtendClientAreaEnabled
         public bool ExtendClientAreaEnabled
         {
         {
             get { return _extendClientAreaEnabled; }
             get { return _extendClientAreaEnabled; }
             set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
             set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
-        }        
+        }
 
 
         public bool SystemTitleBarEnabled
         public bool SystemTitleBarEnabled
         {
         {
             get { return _systemTitleBarEnabled; }
             get { return _systemTitleBarEnabled; }
             set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
             set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
-        }        
+        }
 
 
         public bool PreferSystemChromeEnabled
         public bool PreferSystemChromeEnabled
         {
         {
             get { return _preferSystemChromeEnabled; }
             get { return _preferSystemChromeEnabled; }
             set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
             set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
-        }        
+        }
 
 
         public double TitleBarHeight
         public double TitleBarHeight
         {
         {
@@ -122,6 +126,24 @@ namespace ControlCatalog.ViewModels
             set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
             set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
         }
         }
 
 
+        public bool IsSystemBarVisible
+        {
+            get { return _isSystemBarVisible; }
+            set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
+        }
+
+        public bool DisplayEdgeToEdge
+        {
+            get { return _displayEdgeToEdge; }
+            set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
+        }
+        
+        public Thickness SafeAreaPadding
+        {
+            get { return _safeAreaPadding; }
+            set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
+        }
+
         public MiniCommand AboutCommand { get; }
         public MiniCommand AboutCommand { get; }
 
 
         public MiniCommand ExitCommand { get; }
         public MiniCommand ExitCommand { get; }

+ 8 - 8
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -8,27 +8,27 @@
   <integrationTestApp:MeasureBorder Name="MyBorder">
   <integrationTestApp:MeasureBorder Name="MyBorder">
     <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
     <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
       <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
       <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
-      <TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
+      <TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
                Text="{Binding ClientSize, Mode=OneWay}" />
                Text="{Binding ClientSize, Mode=OneWay}" />
 
 
       <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
       <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
-      <TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
+      <TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
                Text="{Binding FrameSize, Mode=OneWay}" />
                Text="{Binding FrameSize, Mode=OneWay}" />
 
 
       <Label Grid.Column="0" Grid.Row="3">Position</Label>
       <Label Grid.Column="0" Grid.Row="3">Position</Label>
-      <TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
+      <TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
 
 
       <Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
       <Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
-      <TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
+      <TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
 
 
       <Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
       <Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
-      <TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
+      <TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
 
 
       <Label Grid.Column="0" Grid.Row="6">Scaling</Label>
       <Label Grid.Column="0" Grid.Row="6">Scaling</Label>
-      <TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
+      <TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
 
 
       <Label Grid.Column="0" Grid.Row="7">WindowState</Label>
       <Label Grid.Column="0" Grid.Row="7">WindowState</Label>
-      <ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
+      <ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
         <ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
         <ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
         <ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
         <ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
         <ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
         <ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
@@ -36,7 +36,7 @@
       </ComboBox>
       </ComboBox>
 
 
       <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
       <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
-      <TextBox Name="Order" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
+      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
 
 
       <Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
       <Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
       
       

+ 6 - 6
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@@ -35,11 +35,11 @@ namespace IntegrationTestApp
         {
         {
             InitializeComponent();
             InitializeComponent();
             DataContext = this;
             DataContext = this;
-            PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
+            PositionChanged += (s, e) => this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
 
 
             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
             {
-                _orderTextBox = this.GetControl<TextBox>("Order");
+                _orderTextBox = this.GetControl<TextBox>("CurrentOrder");
                 _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
                 _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
                 _timer.Tick += TimerOnTick;
                 _timer.Tick += TimerOnTick;
                 _timer.Start();
                 _timer.Start();
@@ -55,13 +55,13 @@ namespace IntegrationTestApp
         {
         {
             base.OnOpened(e);
             base.OnOpened(e);
             var scaling = PlatformImpl!.DesktopScaling;
             var scaling = PlatformImpl!.DesktopScaling;
-            this.GetControl<TextBox>("Position").Text = $"{Position}";
-            this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
-            this.GetControl<TextBox>("Scaling").Text = $"{scaling}";
+            this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
+            this.GetControl<TextBox>("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
+            this.GetControl<TextBox>("CurrentScaling").Text = $"{scaling}";
 
 
             if (Owner is not null)
             if (Owner is not null)
             {
             {
-                var ownerRect = this.GetControl<TextBox>("OwnerRect");
+                var ownerRect = this.GetControl<TextBox>("CurrentOwnerRect");
                 var owner = (Window)Owner;
                 var owner = (Window)Owner;
                 ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
                 ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
             }
             }

+ 15 - 0
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Diagnostics;
 using Android.App;
 using Android.App;
 using Android.Content;
 using Android.Content;
 using Android.Content.PM;
 using Android.Content.PM;
@@ -32,6 +33,9 @@ namespace Avalonia.Android
                 lifetime.View = View;
                 lifetime.View = View;
             }
             }
 
 
+            Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
+            Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
+
             base.OnCreate(savedInstanceState);
             base.OnCreate(savedInstanceState);
 
 
             SetContentView(View);
             SetContentView(View);
@@ -55,6 +59,17 @@ namespace Avalonia.Android
             }
             }
         }
         }
 
 
+        protected override void OnResume()
+        {
+            base.OnResume();
+
+            // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
+            if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+            {
+                Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges;
+            }
+        }
+
         public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
         public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
 
 
         public override void OnBackPressed()
         public override void OnBackPressed()

+ 6 - 0
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -8,6 +8,7 @@ using Avalonia.Android.Platform;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Controls.Embedding;
 using Avalonia.Controls.Embedding;
+using Avalonia.Controls.Platform;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
 
 
@@ -67,6 +68,11 @@ namespace Avalonia.Android
                 }
                 }
 
 
                 _root.Renderer.Start();
                 _root.Renderer.Start();
+
+                if (_view.TryGetFeature<IInsetsManager>(out var insetsManager) == true)
+                {
+                    (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState();
+                }
             }
             }
             else
             else
             {
             {

+ 235 - 0
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using Android.OS;
+using Android.Views;
+using AndroidX.AppCompat.App;
+using AndroidX.Core.View;
+using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Controls.Platform;
+using static Avalonia.Controls.Platform.IInsetsManager;
+
+namespace Avalonia.Android.Platform
+{
+    internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener
+    {
+        private readonly AvaloniaMainActivity _activity;
+        private readonly TopLevelImpl _topLevel;
+        private readonly InsetsAnimationCallback _callback;
+        private bool _displayEdgeToEdge;
+        private bool _usesLegacyLayouts;
+        private bool? _systemUiVisibility;
+        private SystemBarTheme? _statusBarTheme;
+        private bool? _isDefaultSystemBarLightTheme;
+
+        public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
+
+        public bool DisplayEdgeToEdge
+        {
+            get => _displayEdgeToEdge; 
+            set
+            {
+                _displayEdgeToEdge = value;
+
+                if(Build.VERSION.SdkInt >= BuildVersionCodes.P)
+                {
+                    _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
+                }
+
+                WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
+            }
+        }
+
+        public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel)
+        {
+            _activity = activity;
+            _topLevel = topLevel;
+            _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop);
+
+            _callback.InsetsManager = this;
+
+            ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this);
+
+            ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback);
+
+            if(Build.VERSION.SdkInt < BuildVersionCodes.R)
+            {
+                _usesLegacyLayouts = true;
+                _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this);
+            }
+
+            DisplayEdgeToEdge = false;
+        }
+
+        public Thickness SafeAreaPadding
+        {
+            get
+            {
+                var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
+
+                if (insets != null)
+                {
+                    var renderScaling = _topLevel.RenderScaling;
+
+                    var inset = insets.GetInsets(
+                        (DisplayEdgeToEdge ?
+                            WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
+                            WindowInsetsCompat.Type.DisplayCutout() :
+                            0) | WindowInsetsCompat.Type.Ime());
+                    var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
+                    var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+                    return new Thickness(inset.Left / renderScaling,
+                        inset.Top / renderScaling,
+                        inset.Right / renderScaling,
+                        (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
+                            imeInset.Bottom - navBarInset.Bottom :
+                            inset.Bottom) / renderScaling);
+                }
+
+                return default;
+            }
+        }
+
+        public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
+        {
+            NotifySafeAreaChanged(SafeAreaPadding);
+            return insets;
+        }
+
+        private void NotifySafeAreaChanged(Thickness safeAreaPadding)
+        {
+            SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding));
+        }
+
+        public void OnGlobalLayout()
+        {
+            NotifySafeAreaChanged(SafeAreaPadding);
+        }
+
+        public SystemBarTheme? SystemBarTheme
+        {
+            get
+            {
+                try
+                {
+                    var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+
+                    return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+                }
+                catch (Exception _)
+                {
+                    return Controls.Platform.SystemBarTheme.Light;
+                }
+            }
+            set
+            {
+                _statusBarTheme = value;
+
+                var isDefault = _statusBarTheme == null;
+
+                if (!_topLevel.View.IsShown)
+                {
+                    return;
+                }
+
+                var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+
+                if (_isDefaultSystemBarLightTheme == null)
+                {
+                    _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars;
+                }
+
+                if (value == null && _isDefaultSystemBarLightTheme != null)
+                {
+                    value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+                }
+
+                compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
+                compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light;
+
+                AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
+            }
+        }
+
+        public bool? IsSystemBarVisible
+        {
+            get
+            {
+                if(_activity.Window == null)
+                {
+                    return true;
+                }
+                var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
+
+                return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+            }
+            set
+            {
+                _systemUiVisibility = value;
+
+                if (!_topLevel.View.IsShown)
+                {
+                    return;
+                }
+
+                var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View);
+
+                if (value == null || value.Value)
+                {
+                    compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+                }
+                else
+                {
+                    compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+
+                    if (compat != null)
+                    {
+                        compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe;
+                    }
+                }
+            }
+        }
+
+        internal void ApplyStatusBarState()
+        {
+            IsSystemBarVisible = _systemUiVisibility;
+            SystemBarTheme = _statusBarTheme;
+        }
+
+        private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback
+        {
+            public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode)
+            {
+            }
+
+            public AndroidInsetsManager InsetsManager { get; set; }
+
+            public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
+            {
+                foreach (var anim in runningAnimations)
+                {
+                    if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
+                    {
+                        var renderScaling = InsetsManager._topLevel.RenderScaling;
+
+                        var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime());
+                        var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
+                        var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+
+                        var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom);
+                        bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction);
+
+                        var padding = new Thickness(inset.Left / renderScaling,
+                            inset.Top / renderScaling,
+                            inset.Right / renderScaling,
+                            bottomPadding / renderScaling);
+                        InsetsManager?.NotifySafeAreaChanged(padding);
+                        break;
+                    }
+                }
+                return insets;
+            }
+        }
+    }
+}

+ 24 - 19
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -3,9 +3,7 @@ using System.Collections.Generic;
 using Android.App;
 using Android.App;
 using Android.Content;
 using Android.Content;
 using Android.Graphics;
 using Android.Graphics;
-using Android.OS;
 using Android.Runtime;
 using Android.Runtime;
-using Android.Text;
 using Android.Views;
 using Android.Views;
 using Android.Views.InputMethods;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific;
@@ -24,11 +22,13 @@ using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Avalonia.Rendering.Composition;
 using Java.Lang;
 using Java.Lang;
+using Java.Util;
 using Math = System.Math;
 using Math = System.Math;
 using AndroidRect = Android.Graphics.Rect;
 using AndroidRect = Android.Graphics.Rect;
 using Window = Android.Views.Window;
 using Window = Android.Views.Window;
 using Android.Graphics.Drawables;
 using Android.Graphics.Drawables;
-using Java.Util;
+using Android.OS;
+using Android.Text;
 
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
 {
@@ -43,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly AndroidInsetsManager _insetsManager;
         private ViewImpl _view;
         private ViewImpl _view;
 
 
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@@ -59,6 +60,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
             MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
 
 
+            if (avaloniaView.Context is AvaloniaMainActivity mainActivity)
+            {
+                _insetsManager = new AndroidInsetsManager(mainActivity, this);
+            }
+
             _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
             _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
             _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
             _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
 
 
@@ -70,21 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
 
         public IInputRoot InputRoot { get; private set; }
         public IInputRoot InputRoot { get; private set; }
 
 
-        public virtual Size ClientSize
-        {
-            get
-            {
-                AndroidRect rect = new AndroidRect();
-                AndroidRect intersection = new AndroidRect();
-
-                _view.GetWindowVisibleDisplayFrame(intersection);
-                _view.GetGlobalVisibleRect(rect);
-
-                intersection.Intersect(rect);
-
-                return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling);
-            }
-        }
+        public virtual Size ClientSize => _view.Size.ToSize(RenderScaling);
 
 
         public Size? FrameSize => null;
         public Size? FrameSize => null;
 
 
@@ -285,7 +277,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
 
         public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
         public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
         {
         {
-            // TODO adjust status bar depending on full screen mode.
+            if(_insetsManager != null)
+            {
+                _insetsManager.SystemBarTheme = themeVariant switch
+                {
+                    PlatformThemeVariant.Light => SystemBarTheme.Light,
+                    PlatformThemeVariant.Dark => SystemBarTheme.Dark,
+                    _ => null,
+                };
+            }
         }
         }
 
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
@@ -403,6 +403,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 return _nativeControlHost;
                 return _nativeControlHost;
             }
             }
 
 
+            if (featureType == typeof(IInsetsManager))
+            {
+                return _insetsManager;
+            }
+
             return null;
             return null;
         }
         }
     }
     }

+ 55 - 0
src/Avalonia.Controls/Platform/IInsetsManager.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Metadata;
+
+#nullable enable
+namespace Avalonia.Controls.Platform
+{
+    [Unstable]
+    [NotClientImplementable]
+    public interface IInsetsManager
+    {
+        /// <summary>
+        /// Gets or sets whether the system bars are visible.
+        /// </summary>
+        bool? IsSystemBarVisible { get; set; }
+
+        /// <summary>
+        /// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
+        /// </summary>
+        bool DisplayEdgeToEdge { get; set; }
+
+        /// <summary>
+        /// Gets the current safe area padding.
+        /// </summary>
+        Thickness SafeAreaPadding { get; }
+        
+        /// <summary>
+        /// Occurs when safe area for the current window changes.
+        /// </summary>
+        event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged;
+    }
+    
+    public class SafeAreaChangedArgs : EventArgs
+    {
+        public SafeAreaChangedArgs(Thickness safeArePadding)
+        {
+            SafeAreaPadding = safeArePadding;
+        }
+
+        /// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
+        public Thickness SafeAreaPadding { get; }
+    }
+
+    public enum SystemBarTheme
+    {
+        /// <summary>
+        /// Light system bar theme, with light background and a dark foreground
+        /// </summary>
+        Light,
+
+        /// <summary>
+        /// Bark system bar theme, with dark background and a light foreground
+        /// </summary>
+        Dark
+    }
+}

+ 4 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -15,6 +15,7 @@ using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
+using Avalonia.Reactive;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
@@ -391,7 +392,9 @@ namespace Avalonia.Controls
             ??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
             ??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
             ?? PlatformImpl?.TryGetFeature<IStorageProvider>()
             ?? PlatformImpl?.TryGetFeature<IStorageProvider>()
             ?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
             ?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
-        
+
+        public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         Point IRenderRoot.PointToClient(PixelPoint p)
         Point IRenderRoot.PointToClient(PixelPoint p)
         {
         {

+ 45 - 0
src/Browser/Avalonia.Browser/BrowserInsetsManager.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Browser.Interop;
+using Avalonia.Controls.Platform;
+using static Avalonia.Controls.Platform.IInsetsManager;
+
+namespace Avalonia.Browser
+{
+    internal class BrowserInsetsManager : IInsetsManager
+    {
+        public bool? IsSystemBarVisible
+        {
+            get
+            {
+                return DomHelper.IsFullscreen();
+            }
+            set
+            {
+                DomHelper.SetFullscreen(!value ?? false);
+            }
+        }
+
+        public bool DisplayEdgeToEdge { get; set; }
+
+        public event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged;
+
+        public Thickness SafeAreaPadding
+        {
+            get
+            {
+                var padding = DomHelper.GetSafeAreaPadding();
+
+                return new Thickness(padding[0], padding[1], padding[2], padding[3]);
+            }
+        }
+
+        public void NotifySafeAreaPaddingChanged()
+        {
+            SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
+        }
+    }
+}

+ 11 - 0
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@@ -31,6 +31,7 @@ namespace Avalonia.Browser
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly IInsetsManager? _insetsManager;
 
 
         public BrowserTopLevelImpl(AvaloniaView avaloniaView)
         public BrowserTopLevelImpl(AvaloniaView avaloniaView)
         {
         {
@@ -40,9 +41,12 @@ namespace Avalonia.Browser
             AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
             AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
             _touchDevice = new TouchDevice();
             _touchDevice = new TouchDevice();
             _penDevice = new PenDevice();
             _penDevice = new PenDevice();
+
+            _insetsManager = new BrowserInsetsManager();
             _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
             _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
             _storageProvider = new BrowserStorageProvider();
             _storageProvider = new BrowserStorageProvider();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
+
         }
         }
 
 
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
@@ -69,6 +73,8 @@ namespace Avalonia.Browser
                 }
                 }
 
 
                 Resized?.Invoke(newSize, PlatformResizeReason.User);
                 Resized?.Invoke(newSize, PlatformResizeReason.User);
+
+                (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged();
             }
             }
         }
         }
 
 
@@ -271,6 +277,11 @@ namespace Avalonia.Browser
                 return _nativeControlHost;
                 return _nativeControlHost;
             }
             }
 
 
+            if (featureType == typeof(IInsetsManager))
+            {
+                return _insetsManager;
+            }
+
             return null;
             return null;
         }
         }
     }
     }

+ 9 - 0
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@@ -11,6 +11,15 @@ internal static partial class DomHelper
     [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
     [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
     public static partial JSObject CreateAvaloniaHost(JSObject element);
     public static partial JSObject CreateAvaloniaHost(JSObject element);
 
 
+    [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)]
+    public static partial bool IsFullscreen();
+
+    [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)]
+    public static partial JSObject SetFullscreen(bool isFullscreen);
+
+    [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)]
+    public static partial byte[] GetSafeAreaPadding();
+
     [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
     [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
     public static partial void AddCssClass(JSObject element, string className);
     public static partial void AddCssClass(JSObject element, string className);
 
 

+ 22 - 0
src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

@@ -84,4 +84,26 @@ export class AvaloniaDOM {
             inputElement
             inputElement
         };
         };
     }
     }
+
+    public static isFullscreen(): boolean {
+        return document.fullscreenElement != null;
+    }
+
+    public static async setFullscreen(isFullscreen: boolean) {
+        if (isFullscreen) {
+            const doc = document.documentElement;
+            await doc.requestFullscreen();
+        } else {
+            await document.exitFullscreen();
+        }
+    }
+
+    public static getSafeAreaPadding(): number[] {
+        const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat"));
+        const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab"));
+        const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal"));
+        const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar"));
+
+        return [left, top, bottom, right];
+    }
 }
 }

+ 3 - 1
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@@ -40,10 +40,12 @@ namespace Avalonia.iOS
                 
                 
                 var view = new AvaloniaView();
                 var view = new AvaloniaView();
                 lifetime.View = view;
                 lifetime.View = view;
-                Window.RootViewController = new UIViewController
+                var controller = new DefaultAvaloniaViewController
                 {
                 {
                     View = view
                     View = view
                 };
                 };
+                Window.RootViewController = controller;
+                view.InitWithController(controller);
             });
             });
             
             
             builder.SetupWithLifetime(lifetime);
             builder.SetupWithLifetime(lifetime);

+ 28 - 7
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -16,6 +16,7 @@ using Foundation;
 using ObjCRuntime;
 using ObjCRuntime;
 using OpenGLES;
 using OpenGLES;
 using UIKit;
 using UIKit;
+using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager;
 
 
 namespace Avalonia.iOS
 namespace Avalonia.iOS
 {
 {
@@ -26,6 +27,7 @@ namespace Avalonia.iOS
         private EmbeddableControlRoot _topLevel;
         private EmbeddableControlRoot _topLevel;
         private TouchHandler _touches;
         private TouchHandler _touches;
         private ITextInputMethodClient _client;
         private ITextInputMethodClient _client;
+        private IAvaloniaViewController _controller;
 
 
         public AvaloniaView()
         public AvaloniaView()
         {
         {
@@ -48,10 +50,13 @@ namespace Avalonia.iOS
             MultipleTouchEnabled = true;
             MultipleTouchEnabled = true;
         }
         }
 
 
+        /// <inheritdoc />
         public override bool CanBecomeFirstResponder => true;
         public override bool CanBecomeFirstResponder => true;
 
 
+        /// <inheritdoc />
         public override bool CanResignFirstResponder => true;
         public override bool CanResignFirstResponder => true;
 
 
+        /// <inheritdoc />
         public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
         public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
         {
         {
             base.TraitCollectionDidChange(previousTraitCollection);
             base.TraitCollectionDidChange(previousTraitCollection);
@@ -60,6 +65,7 @@ namespace Avalonia.iOS
             settings?.TraitCollectionDidChange();
             settings?.TraitCollectionDidChange();
         }
         }
 
 
+        /// <inheritdoc />
         public override void TintColorDidChange()
         public override void TintColorDidChange()
         {
         {
             base.TintColorDidChange();
             base.TintColorDidChange();
@@ -68,18 +74,31 @@ namespace Avalonia.iOS
             settings?.TraitCollectionDidChange();
             settings?.TraitCollectionDidChange();
         }
         }
 
 
+        public void InitWithController<TController>(TController controller)
+            where TController : UIViewController, IAvaloniaViewController
+        {
+            _controller = controller;
+            _topLevelImpl._insetsManager.InitWithController(controller);
+        }
+        
         internal class TopLevelImpl : ITopLevelImpl
         internal class TopLevelImpl : ITopLevelImpl
         {
         {
             private readonly AvaloniaView _view;
             private readonly AvaloniaView _view;
             private readonly INativeControlHostImpl _nativeControlHost;
             private readonly INativeControlHostImpl _nativeControlHost;
             private readonly IStorageProvider _storageProvider;
             private readonly IStorageProvider _storageProvider;
+            internal readonly InsetsManager _insetsManager;
             public AvaloniaView View => _view;
             public AvaloniaView View => _view;
 
 
             public TopLevelImpl(AvaloniaView view)
             public TopLevelImpl(AvaloniaView view)
             {
             {
                 _view = view;
                 _view = view;
-                _nativeControlHost = new NativeControlHostImpl(_view);
+                _nativeControlHost = new NativeControlHostImpl(view);
                 _storageProvider = new IOSStorageProvider(view);
                 _storageProvider = new IOSStorageProvider(view);
+                _insetsManager = new InsetsManager(view);
+                _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) =>
+                {
+                    view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding;
+                };
             }
             }
 
 
             public void Dispose()
             public void Dispose()
@@ -141,17 +160,14 @@ namespace Avalonia.iOS
             public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
             public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
             {
             {
                 // TODO adjust status bar depending on full screen mode.
                 // TODO adjust status bar depending on full screen mode.
-                if (OperatingSystem.IsIOSVersionAtLeast(13))
+                if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null)
                 {
                 {
-                    var uiStatusBarStyle = themeVariant switch
+                    _view._controller.PreferredStatusBarStyle = themeVariant switch
                     {
                     {
                         PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent,
                         PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent,
                         PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent,
                         PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent,
-                        _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null)
+                        _ => UIStatusBarStyle.Default
                     };
                     };
-                    
-                    // Consider using UIViewController.PreferredStatusBarStyle in the future.
-                    UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true);
                 }
                 }
             }
             }
             
             
@@ -175,6 +191,11 @@ namespace Avalonia.iOS
                     return _nativeControlHost;
                     return _nativeControlHost;
                 }
                 }
 
 
+                if (featureType == typeof(IInsetsManager))
+                {
+                    return _insetsManager;
+                }
+
                 return null;
                 return null;
             }
             }
         }
         }

+ 83 - 0
src/iOS/Avalonia.iOS/InsetsManager.cs

@@ -0,0 +1,83 @@
+using System;
+using Avalonia.Controls.Platform;
+using UIKit;
+
+namespace Avalonia.iOS;
+#nullable enable
+
+internal class InsetsManager : IInsetsManager
+{
+    private readonly AvaloniaView _view;
+    private IAvaloniaViewController? _controller;
+    private bool _displayEdgeToEdge;
+
+    public InsetsManager(AvaloniaView view)
+    {
+        _view = view;
+    }
+
+    internal void InitWithController(IAvaloniaViewController controller)
+    {
+        _controller = controller;
+        if (_controller is not null)
+        {
+            _controller.SafeAreaPaddingChanged += (_, _) =>
+            {
+                SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
+                DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge);
+            };
+        }
+    }
+
+    public SystemBarTheme? SystemBarTheme
+    {
+        get => _controller?.PreferredStatusBarStyle switch
+        {
+            UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark,
+            UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light,
+            _ => null
+        };
+        set
+        {
+            if (_controller != null)
+            {
+                _controller.PreferredStatusBarStyle = value switch
+                {
+                    Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent,
+                    Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent,
+                    null => UIStatusBarStyle.Default,
+                    _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
+                };
+            }
+        }
+    }
+
+    public bool? IsSystemBarVisible
+    {
+        get => _controller?.PrefersStatusBarHidden == false;
+        set
+        {
+            if (_controller is not null)
+            {
+                _controller.PrefersStatusBarHidden = value == false;
+            }
+        }
+    }
+    public event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged;
+    public event EventHandler<bool>? DisplayEdgeToEdgeChanged;
+
+    public bool DisplayEdgeToEdge
+    {
+        get => _displayEdgeToEdge;
+        set
+        {
+            if (_displayEdgeToEdge != value)
+            {
+                _displayEdgeToEdge = value;
+                DisplayEdgeToEdgeChanged?.Invoke(this, value);
+            }
+        }
+    }
+
+    public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default;
+}

+ 74 - 0
src/iOS/Avalonia.iOS/ViewController.cs

@@ -0,0 +1,74 @@
+using System;
+using Avalonia.Metadata;
+using UIKit;
+
+namespace Avalonia.iOS;
+
+[Unstable]
+public interface IAvaloniaViewController
+{
+    UIStatusBarStyle PreferredStatusBarStyle { get; set; }
+    bool PrefersStatusBarHidden { get; set; }
+    Thickness SafeAreaPadding { get; }
+    event EventHandler SafeAreaPaddingChanged;
+}
+
+/// <inheritdoc cref="IAvaloniaViewController" />
+public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController
+{
+    private UIStatusBarStyle? _preferredStatusBarStyle;
+    private bool? _prefersStatusBarHidden;
+    
+    /// <inheritdoc/>
+    public override void ViewDidLayoutSubviews()
+    {
+        base.ViewDidLayoutSubviews();
+        var size = View?.Frame.Size ?? default;
+        var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default;
+        var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom);
+        if (SafeAreaPadding != safeArea)
+        {
+            SafeAreaPadding = safeArea;
+            SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty);
+        }
+    }
+
+    /// <inheritdoc/>
+    public override bool PrefersStatusBarHidden()
+    {
+        return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden();
+    }
+
+    /// <inheritdoc/>
+    public override UIStatusBarStyle PreferredStatusBarStyle()
+    {
+        // don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style.
+        return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle();
+    }
+
+    UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle
+    {
+        get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default;
+        set
+        {
+            _preferredStatusBarStyle = value;
+            SetNeedsStatusBarAppearanceUpdate();
+        }
+    }
+
+    bool IAvaloniaViewController.PrefersStatusBarHidden
+    {
+        get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados
+        set
+        {
+            _prefersStatusBarHidden = value;
+            SetNeedsStatusBarAppearanceUpdate();
+        }
+    }
+
+    /// <inheritdoc/>
+    public Thickness SafeAreaPadding { get; private set; }
+
+    /// <inheritdoc/>
+    public event EventHandler SafeAreaPaddingChanged;
+}

+ 10 - 10
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@@ -93,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium
                 {
                 {
                     try
                     try
                     {
                     {
-                        _session.FindElementByAccessibilityId("WindowState").SendClick();
+                        _session.FindElementByAccessibilityId("CurrentWindowState").SendClick();
                         _session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
                         _session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
 
 
                         // Wait for animations to run.
                         // Wait for animations to run.
@@ -113,7 +113,7 @@ namespace Avalonia.IntegrationTests.Appium
         {
         {
             using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
             using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
             {
             {
-                var windowState = _session.FindElementByAccessibilityId("WindowState");
+                var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
 
 
                 Assert.Equal("Normal", windowState.GetComboBoxValue());
                 Assert.Equal("Normal", windowState.GetComboBoxValue());
                 
                 
@@ -170,7 +170,7 @@ namespace Avalonia.IntegrationTests.Appium
         public void ShowMode(ShowWindowMode mode)
         public void ShowMode(ShowWindowMode mode)
         {
         {
             using var window = OpenWindow(null, mode, WindowStartupLocation.Manual);
             using var window = OpenWindow(null, mode, WindowStartupLocation.Manual);
-            var windowState = _session.FindElementByAccessibilityId("WindowState");
+            var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
             var original = GetWindowInfo();
             var original = GetWindowInfo();
 
 
             Assert.Equal("Normal", windowState.GetComboBoxValue());
             Assert.Equal("Normal", windowState.GetComboBoxValue());
@@ -373,7 +373,7 @@ namespace Avalonia.IntegrationTests.Appium
         {
         {
             PixelRect? ReadOwnerRect()
             PixelRect? ReadOwnerRect()
             {
             {
-                var text = _session.FindElementByAccessibilityId("OwnerRect").Text;
+                var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text;
                 return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
                 return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
             }
             }
 
 
@@ -384,13 +384,13 @@ namespace Avalonia.IntegrationTests.Appium
                 try
                 try
                 {
                 {
                     return new(
                     return new(
-                        Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text),
-                        Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text),
-                        PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text),
+                        Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text),
+                        Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text),
+                        PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text),
                         ReadOwnerRect(),
                         ReadOwnerRect(),
-                        PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text),
-                        double.Parse(_session.FindElementByAccessibilityId("Scaling").Text),
-                        Enum.Parse<WindowState>(_session.FindElementByAccessibilityId("WindowState").Text));
+                        PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text),
+                        double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text),
+                        Enum.Parse<WindowState>(_session.FindElementByAccessibilityId("CurrentWindowState").Text));
                 }
                 }
                 catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3)
                 catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3)
                 {
                 {

+ 1 - 1
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@@ -393,7 +393,7 @@ namespace Avalonia.IntegrationTests.Appium
         private int GetWindowOrder(string identifier)
         private int GetWindowOrder(string identifier)
         {
         {
             var window = GetWindow(identifier);
             var window = GetWindow(identifier);
-            var order = window.FindElementByXPath("//*[@identifier='Order']");
+            var order = window.FindElementByXPath("//*[@identifier='CurrentOrder']");
             return int.Parse(order.Text);
             return int.Parse(order.Text);
         }
         }