Browse Source

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

# Conflicts:
#	samples/IntegrationTestApp/ShowWindowTest.axaml
pull/10532/head
Dan Walmsley 3 years ago
parent
commit
50b11709be
  1. 2
      samples/ControlCatalog.Android/Resources/values/styles.xml
  2. 9
      samples/ControlCatalog.Browser/app.css
  3. 4
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  4. 6
      samples/ControlCatalog.iOS/Info.plist
  5. 4
      samples/ControlCatalog/App.xaml.cs
  6. 38
      samples/ControlCatalog/MainView.xaml.cs
  7. 1
      samples/ControlCatalog/MainWindow.xaml.cs
  8. 22
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  9. 30
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  10. 16
      samples/IntegrationTestApp/ShowWindowTest.axaml
  11. 12
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  12. 15
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  13. 6
      src/Android/Avalonia.Android/AvaloniaView.cs
  14. 235
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  15. 43
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  16. 55
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  17. 5
      src/Avalonia.Controls/TopLevel.cs
  18. 45
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  19. 11
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  20. 9
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  21. 22
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
  22. 4
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  23. 35
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  24. 83
      src/iOS/Avalonia.iOS/InsetsManager.cs
  25. 74
      src/iOS/Avalonia.iOS/ViewController.cs
  26. 20
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  27. 2
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

2
samples/ControlCatalog.Android/Resources/values/styles.xml

@ -4,7 +4,7 @@
<style name="MyTheme">
</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:windowNoTitle">true</item>
</style>

9
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;
width: 100vw
}

4
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

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

6
samples/ControlCatalog.iOS/Info.plist

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

4
samples/ControlCatalog/App.xaml.cs

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

38
samples/ControlCatalog/MainView.xaml.cs

@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@ -12,6 +13,7 @@ using Avalonia.VisualTree;
using Avalonia.Styling;
using ControlCatalog.Models;
using ControlCatalog.Pages;
using ControlCatalog.ViewModels;
namespace ControlCatalog
{
@ -99,13 +101,47 @@ namespace ControlCatalog
};
}
internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var decorations = this.Get<ComboBox>("Decorations");
if (VisualRoot is Window window)
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;
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
}

1
samples/ControlCatalog/MainWindow.xaml.cs

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

22
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -5,11 +5,21 @@
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
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>
</UserControl>

30
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

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

16
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -8,27 +8,27 @@
<integrationTestApp:MeasureBorder Name="MyBorder">
<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>
<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}" />
<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}" />
<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>
<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>
<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>
<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>
<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="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
@ -36,7 +36,7 @@
</ComboBox>
<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>

12
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -35,11 +35,11 @@ namespace IntegrationTestApp
{
InitializeComponent();
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))
{
_orderTextBox = this.GetControl<TextBox>("Order");
_orderTextBox = this.GetControl<TextBox>("CurrentOrder");
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
_timer.Tick += TimerOnTick;
_timer.Start();
@ -55,13 +55,13 @@ namespace IntegrationTestApp
{
base.OnOpened(e);
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)
{
var ownerRect = this.GetControl<TextBox>("OwnerRect");
var ownerRect = this.GetControl<TextBox>("CurrentOwnerRect");
var owner = (Window)Owner;
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}

15
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Android.App;
using Android.Content;
using Android.Content.PM;
@ -32,6 +33,9 @@ namespace Avalonia.Android
lifetime.View = View;
}
Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
base.OnCreate(savedInstanceState);
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 override void OnBackPressed()

6
src/Android/Avalonia.Android/AvaloniaView.cs

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

235
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;
}
}
}
}

43
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -3,9 +3,7 @@ using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.Specific;
@ -24,11 +22,13 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using Java.Util;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
using Java.Util;
using Android.OS;
using Android.Text;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -43,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager _insetsManager;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -59,6 +60,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
if (avaloniaView.Context is AvaloniaMainActivity mainActivity)
{
_insetsManager = new AndroidInsetsManager(mainActivity, this);
}
_nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
_storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
@ -70,21 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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;
@ -285,7 +277,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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);
@ -403,6 +403,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _nativeControlHost;
}
if (featureType == typeof(IInsetsManager))
{
return _insetsManager;
}
return null;
}
}

55
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
}
}

5
src/Avalonia.Controls/TopLevel.cs

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

45
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
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

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

9
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@ -11,6 +11,15 @@ internal static partial class DomHelper
[JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
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)]
public static partial void AddCssClass(JSObject element, string className);

22
src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

@ -84,4 +84,26 @@ export class AvaloniaDOM {
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];
}
}

4
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

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

35
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -16,6 +16,7 @@ using Foundation;
using ObjCRuntime;
using OpenGLES;
using UIKit;
using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager;
namespace Avalonia.iOS
{
@ -26,6 +27,7 @@ namespace Avalonia.iOS
private EmbeddableControlRoot _topLevel;
private TouchHandler _touches;
private ITextInputMethodClient _client;
private IAvaloniaViewController _controller;
public AvaloniaView()
{
@ -48,10 +50,13 @@ namespace Avalonia.iOS
MultipleTouchEnabled = true;
}
/// <inheritdoc />
public override bool CanBecomeFirstResponder => true;
/// <inheritdoc />
public override bool CanResignFirstResponder => true;
/// <inheritdoc />
public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
{
base.TraitCollectionDidChange(previousTraitCollection);
@ -60,6 +65,7 @@ namespace Avalonia.iOS
settings?.TraitCollectionDidChange();
}
/// <inheritdoc />
public override void TintColorDidChange()
{
base.TintColorDidChange();
@ -68,18 +74,31 @@ namespace Avalonia.iOS
settings?.TraitCollectionDidChange();
}
public void InitWithController<TController>(TController controller)
where TController : UIViewController, IAvaloniaViewController
{
_controller = controller;
_topLevelImpl._insetsManager.InitWithController(controller);
}
internal class TopLevelImpl : ITopLevelImpl
{
private readonly AvaloniaView _view;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
internal readonly InsetsManager _insetsManager;
public AvaloniaView View => _view;
public TopLevelImpl(AvaloniaView view)
{
_view = view;
_nativeControlHost = new NativeControlHostImpl(_view);
_nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(view);
_insetsManager = new InsetsManager(view);
_insetsManager.DisplayEdgeToEdgeChanged += (sender, b) =>
{
view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding;
};
}
public void Dispose()
@ -141,17 +160,14 @@ namespace Avalonia.iOS
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// 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.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;
}
if (featureType == typeof(IInsetsManager))
{
return _insetsManager;
}
return null;
}
}

83
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
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;
}

20
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@ -93,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium
{
try
{
_session.FindElementByAccessibilityId("WindowState").SendClick();
_session.FindElementByAccessibilityId("CurrentWindowState").SendClick();
_session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
// Wait for animations to run.
@ -113,7 +113,7 @@ namespace Avalonia.IntegrationTests.Appium
{
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());
@ -170,7 +170,7 @@ namespace Avalonia.IntegrationTests.Appium
public void ShowMode(ShowWindowMode mode)
{
using var window = OpenWindow(null, mode, WindowStartupLocation.Manual);
var windowState = _session.FindElementByAccessibilityId("WindowState");
var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
var original = GetWindowInfo();
Assert.Equal("Normal", windowState.GetComboBoxValue());
@ -373,7 +373,7 @@ namespace Avalonia.IntegrationTests.Appium
{
PixelRect? ReadOwnerRect()
{
var text = _session.FindElementByAccessibilityId("OwnerRect").Text;
var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text;
return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
}
@ -384,13 +384,13 @@ namespace Avalonia.IntegrationTests.Appium
try
{
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(),
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)
{

2
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

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

Loading…
Cancel
Save