Преглед изворни кода

Add Platform InputPane interface (#13363)

* android - remove keyboard size from safe area padding

* add software keyboard listener

* change keyboard height to keyboard rect

* rename height members in software keyboard listener to bounds

* fix docs

* update api

* update toplevel input pane propety name

* remove redundant attribute from input pane interface

* Add iOS implementation

* Fix iOS event

* Change iOS easing functions

* Update demo

* Make StartRect nullable as it's not available on every platform

* Windows IInputPane implementation

* Implement browser input pane

* Minor fixes

* Apply suggestions from code review

Co-authored-by: workgroupengineering <[email protected]>

* Update src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs

Co-authored-by: workgroupengineering <[email protected]>

* Dispose InputPane just in case

* Fix build error, replace ref with in

* Fix relative keyboard geometrychange on browser

* Fix compile error because of the wrong style

---------

Co-authored-by: Max Katz <[email protected]>
Co-authored-by: Jumar Macato <[email protected]>
Co-authored-by: workgroupengineering <[email protected]>
Emmanuel Hansen пре 1 година
родитељ
комит
6d9780c5b2

+ 51 - 2
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@@ -1,4 +1,6 @@
-using Avalonia;
+using System;
+using Avalonia;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using MiniMvvm;
@@ -12,7 +14,26 @@ namespace SafeAreaDemo.ViewModels
         private IInsetsManager? _insetsManager;
         private bool _hideSystemBars;
         private bool _autoSafeAreaPadding;
+        private IInputPane? _inputPane;
 
+        public InputPaneState InputPaneState
+        {
+            get
+            {
+                return _inputPane?.State ?? InputPaneState.Closed;
+            }
+        }
+
+        public IEasing? InputPaneEasing { get; private set; }
+        public TimeSpan? InputPaneDuration { get; private set; }
+
+        public Thickness InputPaneMarkerMargin => InputPaneState == InputPaneState.Open
+            ? new Thickness(0, 0, 0, Math.Max(0, CanvasSize.Height - InputPaneRect.Top))
+            : default;
+        public Rect InputPaneRect => _inputPane?.OccludedRect ?? default;
+
+        public Rect CanvasSize { get; set; }
+        
         public Thickness SafeAreaPadding
         {
             get
@@ -90,12 +111,16 @@ namespace SafeAreaDemo.ViewModels
             }
         }
 
-        internal void Initialize(Control mainView, IInsetsManager? InsetsManager)
+        internal void Initialize(Control mainView, IInsetsManager? InsetsManager, IInputPane? inputPane)
         {
             if (_insetsManager != null)
             {
                 _insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
             }
+            if (_inputPane != null)
+            {
+                _inputPane.StateChanged -= InputPaneOnStateChanged;
+            }
 
             _autoSafeAreaPadding = mainView.GetValue(TopLevel.AutoSafeAreaPaddingProperty);
             _insetsManager = InsetsManager;
@@ -107,6 +132,20 @@ namespace SafeAreaDemo.ViewModels
                 _displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
                 _hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
             }
+
+            _inputPane = inputPane;
+            if (_inputPane != null)
+            {
+                _inputPane.StateChanged += InputPaneOnStateChanged;
+            }
+            RaiseKeyboardChanged();
+        }
+
+        private void InputPaneOnStateChanged(object? sender, InputPaneStateEventArgs e)
+        {
+            InputPaneDuration = e.AnimationDuration;
+            InputPaneEasing = e.Easing ?? new LinearEasing();
+            RaiseKeyboardChanged();
         }
 
         private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e)
@@ -118,6 +157,16 @@ namespace SafeAreaDemo.ViewModels
         {
             this.RaisePropertyChanged(nameof(SafeAreaPadding));
             this.RaisePropertyChanged(nameof(ViewPadding));
+            this.RaisePropertyChanged(nameof(InputPaneMarkerMargin));
+        }
+        
+        private void RaiseKeyboardChanged()
+        {
+            this.RaisePropertyChanged(nameof(InputPaneState));
+            this.RaisePropertyChanged(nameof(InputPaneRect));
+            this.RaisePropertyChanged(nameof(InputPaneEasing));
+            this.RaisePropertyChanged(nameof(InputPaneDuration));
+            this.RaisePropertyChanged(nameof(InputPaneMarkerMargin));
         }
     }
 }

+ 16 - 4
samples/SafeAreaDemo/Views/MainView.xaml

@@ -11,10 +11,11 @@
              Background="#ccc"
              TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
     <Grid HorizontalAlignment="Stretch"
-          VerticalAlignment="Stretch">
-        <Border BorderBrush="Red"
-                Margin="{Binding ViewPadding}"
-                BorderThickness="1">
+          VerticalAlignment="Stretch"
+          Bounds="{Binding CanvasSize, Mode=OneWayToSource}">
+      <Border BorderBrush="Red"
+              Margin="{Binding ViewPadding}"
+              BorderThickness="1">
             <Grid>
             <Label Margin="5"
                    Foreground="Red"
@@ -51,5 +52,16 @@
                 </Grid>
             </DockPanel>
         </Border>
+        <Button Margin="{Binding InputPaneMarkerMargin}"
+                VerticalAlignment="Bottom"
+                Content="X">
+          <Button.Transitions>
+            <Transitions>
+              <ThicknessTransition Property="Margin"
+                                   Duration="{Binding InputPaneDuration}"
+                                   Easing="{Binding InputPaneEasing}"/>
+            </Transitions>
+          </Button.Transitions>
+        </Button>
     </Grid>
 </UserControl>

+ 2 - 1
samples/SafeAreaDemo/Views/MainView.xaml.cs

@@ -18,8 +18,9 @@ namespace SafeAreaDemo.Views
             base.OnLoaded(e);
 
             var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
+            var inputPane = TopLevel.GetTopLevel(this)?.InputPane;
             var viewModel = new MainViewModel();
-            viewModel.Initialize(this, insetsManager);
+            viewModel.Initialize(this, insetsManager, inputPane);
             DataContext = viewModel;
         }
     }

+ 114 - 69
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@@ -2,53 +2,68 @@
 using System.Collections.Generic;
 using Android.OS;
 using Android.Views;
+using Android.Views.Animations;
 using AndroidX.Core.View;
 using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls.Platform;
 using Avalonia.Media;
+using AndroidWindow = Android.Views.Window;
 
 namespace Avalonia.Android.Platform
 {
-    internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener
+    internal sealed class AndroidInsetsManager : WindowInsetsAnimationCompat.Callback, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener, IInputPane
     {
         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;
         private Color? _systemBarColor;
+        private InputPaneState _state;
+        private Rect _previousRect;
+        private readonly bool _usesLegacyLayouts;
 
+        private AndroidWindow Window => _activity.Window ?? throw new InvalidOperationException("Activity.Window must be set."); 
+        
         public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
+        public event EventHandler<InputPaneStateEventArgs> StateChanged;
+
+        public InputPaneState State
+        {
+            get => _state; set
+            {
+                var oldState = _state;
+                _state = value;
+
+                if (oldState != value && Build.VERSION.SdkInt <= BuildVersionCodes.Q)
+                {
+                    var currentRect = OccludedRect;
+                    StateChanged?.Invoke(this, new InputPaneStateEventArgs(value, _previousRect, currentRect, TimeSpan.Zero, null));
+                    _previousRect = currentRect;
+                }
+            }
+        }
 
         public bool DisplayEdgeToEdge
         {
-            get => _displayEdgeToEdge; 
+            get => _displayEdgeToEdge;
             set
             {
                 _displayEdgeToEdge = value;
 
-                var window = _activity.Window;
-
-                if (OperatingSystem.IsAndroidVersionAtLeast(28) && window?.Attributes is { } attributes)
+                if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
                 {
                     attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
                 }
 
-                if (window is not null)
-                {
-                    WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
-                }
+                WindowCompat.SetDecorFitsSystemWindows(Window, !value);
 
-                if(value)
+                if (value)
                 {
-                    if (window is not null)
-                    {
-                        window.AddFlags(WindowManagerFlags.TranslucentStatus);
-                        window.AddFlags(WindowManagerFlags.TranslucentNavigation);
-                    }
+                    Window.AddFlags(WindowManagerFlags.TranslucentStatus);
+                    Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
                 }
                 else
                 {
@@ -57,20 +72,12 @@ namespace Avalonia.Android.Platform
             }
         }
 
-        public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel)
+        internal AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) : base(DispatchModeStop)
         {
             _activity = activity;
             _topLevel = topLevel;
-            _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop);
-
-            _callback.InsetsManager = this;
-
-            if (_activity.Window is { } window)
-            {
-                ViewCompat.SetOnApplyWindowInsetsListener(window.DecorView, this);
 
-                ViewCompat.SetWindowInsetsAnimationCallback(window.DecorView, _callback);
-            }
+            ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView, this);
 
             if (Build.VERSION.SdkInt < BuildVersionCodes.R)
             {
@@ -79,32 +86,48 @@ namespace Avalonia.Android.Platform
             }
 
             DisplayEdgeToEdge = false;
+
+            ViewCompat.SetWindowInsetsAnimationCallback(Window.DecorView, this);
         }
 
         public Thickness SafeAreaPadding
         {
             get
             {
-                var insets = _activity.Window is { } window ? ViewCompat.GetRootWindowInsets(window.DecorView) : null;
+                var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
 
                 if (insets != null)
                 {
                     var renderScaling = _topLevel.RenderScaling;
 
                     var inset = insets.GetInsets(
-                        (_displayEdgeToEdge ?
+                        _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());
+                            WindowInsetsCompat.Type.DisplayCutout() : 0);
 
                     return new Thickness(inset.Left / renderScaling,
                         inset.Top / renderScaling,
                         inset.Right / renderScaling,
-                        (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
-                            imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
-                            inset.Bottom) / renderScaling);
+                        inset.Bottom / renderScaling);
+                }
+
+                return default;
+            }
+        }
+
+        public Rect OccludedRect
+        {
+            get
+            {
+                var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
+
+                if (insets != null)
+                {
+                    var navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
+
+                    var height = Math.Max((float)((insets.GetInsets(WindowInsetsCompat.Type.Ime()).Bottom - navbarInset) / _topLevel.RenderScaling), 0);
+
+                    return new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
                 }
 
                 return default;
@@ -113,8 +136,16 @@ namespace Avalonia.Android.Platform
 
         public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
         {
-            NotifySafeAreaChanged(SafeAreaPadding);
             insets = ViewCompat.OnApplyWindowInsets(v, insets);
+            NotifySafeAreaChanged(SafeAreaPadding);
+
+            if (_previousRect == default)
+            {
+                _previousRect = OccludedRect;
+            }
+
+            State = insets.IsVisible(WindowInsetsCompat.Type.Ime()) ? InputPaneState.Open : InputPaneState.Closed;
+
             return insets;
         }
 
@@ -126,6 +157,12 @@ namespace Avalonia.Android.Platform
         public void OnGlobalLayout()
         {
             NotifySafeAreaChanged(SafeAreaPadding);
+
+            if (_usesLegacyLayouts)
+            {
+                var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
+                State = insets?.IsVisible(WindowInsetsCompat.Type.Ime()) == true ? InputPaneState.Open : InputPaneState.Closed;
+            }
         }
 
         public SystemBarTheme? SystemBarTheme
@@ -134,7 +171,7 @@ namespace Avalonia.Android.Platform
             {
                 try
                 {
-                    var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+                    var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
 
                     return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
                 }
@@ -152,7 +189,7 @@ namespace Avalonia.Android.Platform
                     return;
                 }
 
-                var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+                var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
 
                 if (_isDefaultSystemBarLightTheme == null)
                 {
@@ -161,7 +198,7 @@ namespace Avalonia.Android.Platform
 
                 if (value == null)
                 {
-                    value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+                    value = _isDefaultSystemBarLightTheme.Value ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
                 }
 
                 compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
@@ -173,7 +210,7 @@ namespace Avalonia.Android.Platform
         {
             get
             {
-                if(_activity.Window == null)
+                if (_activity.Window == null)
                 {
                     return true;
                 }
@@ -190,7 +227,7 @@ namespace Avalonia.Android.Platform
                     return;
                 }
 
-                var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View);
+                var compat = WindowCompat.GetInsetsController(Window, _topLevel.View);
 
                 if (value == null || value.Value)
                 {
@@ -210,7 +247,7 @@ namespace Avalonia.Android.Platform
 
         public Color? SystemBarColor
         {
-            get => _systemBarColor; 
+            get => _systemBarColor;
             set
             {
                 _systemBarColor = value;
@@ -240,40 +277,48 @@ namespace Avalonia.Android.Platform
             SystemBarColor = _systemBarColor;
         }
 
-        private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback
+        public override WindowInsetsAnimationCompat.BoundsCompat OnStart(WindowInsetsAnimationCompat animation, WindowInsetsAnimationCompat.BoundsCompat bounds)
         {
-            public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode)
+            if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
             {
-            }
-
-            public AndroidInsetsManager InsetsManager { get; set; }
+                var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
 
-            public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
-            {
-                foreach (var anim in runningAnimations)
+                if (insets != null)
                 {
-                    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 navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
+                    var height = Math.Max(0, (float)((bounds.LowerBound.Bottom - navbarInset) / _topLevel.RenderScaling));
+                    var upperRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
+                    height = Math.Max(0, (float)((bounds.UpperBound.Bottom - navbarInset) / _topLevel.RenderScaling));
+                    var lowerRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
 
+                    var duration = TimeSpan.FromMilliseconds(animation.DurationMillis);
 
-                        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;
-                    }
+                    bool isOpening = State == InputPaneState.Open;
+                    StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration, new AnimationEasing(animation.Interpolator)));
                 }
-                return insets;
             }
+
+            return base.OnStart(animation, bounds);
+        }
+
+        public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
+        {
+            return insets;
+        }
+    }
+
+    internal sealed class AnimationEasing : Easing
+    {
+        private readonly IInterpolator _interpolator;
+
+        public AnimationEasing(IInterpolator interpolator)
+        {
+            _interpolator = interpolator;
+        }
+
+        public override double Ease(double progress)
+        {
+            return _interpolator.GetInterpolation((float)progress);
         }
     }
 }

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -395,7 +395,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 return _nativeControlHost;
             }
 
-            if (featureType == typeof(IInsetsManager))
+            if (featureType == typeof(IInsetsManager) || featureType == typeof(IInputPane))
             {
                 return _insetsManager;
             }

+ 89 - 0
src/Avalonia.Controls/Platform/IInputPane.cs

@@ -0,0 +1,89 @@
+using System;
+using Avalonia.Animation.Easings;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Platform
+{
+    /// <summary>
+    /// Listener for the platform's input pane(eg, software keyboard). Provides access to the input pane height and state.
+    /// </summary>
+    [NotClientImplementable]
+    public interface IInputPane
+    {
+        /// <summary>
+        /// The current input pane state
+        /// </summary>
+        InputPaneState State { get; }
+
+        /// <summary>
+        /// The current input pane bounds.
+        /// </summary>
+        Rect OccludedRect { get; }
+
+        /// <summary>
+        /// Occurs when the input pane's state has changed.
+        /// </summary>
+        event EventHandler<InputPaneStateEventArgs>? StateChanged;
+    }
+
+    /// <summary>
+    /// The input pane opened state.
+    /// </summary>
+    public enum InputPaneState
+    {
+        /// <summary>
+        /// The input pane is either closed, or doesn't form part of the platform insets, i.e. it's floating or is an overlay.
+        /// </summary>
+        Closed,
+
+        /// <summary>
+        /// The input pane is open.
+        /// </summary>
+        Open
+    }
+
+    /// <summary>
+    /// Provides state change information about the input pane.
+    /// </summary>
+    public sealed class InputPaneStateEventArgs : EventArgs
+    {
+        /// <summary>
+        /// The new state of the input pane
+        /// </summary>
+        public InputPaneState NewState { get; }
+
+        /// <summary>
+        /// The initial bounds of the input pane.
+        /// </summary>
+        public Rect? StartRect { get; }
+
+        /// <summary>
+        /// The final bounds of the input pane.
+        /// </summary>
+        public Rect EndRect { get; }
+
+        /// <summary>
+        /// The duration of the input pane's state change animation.
+        /// </summary>
+        public TimeSpan AnimationDuration { get; }
+
+        /// <summary>
+        /// The easing of the input pane's state changed animation.
+        /// </summary>
+        public IEasing? Easing { get; }
+
+        public InputPaneStateEventArgs(InputPaneState newState, Rect? startRect, Rect endRect, TimeSpan animationDuration, IEasing? easing)
+        {
+            NewState = newState;
+            StartRect = startRect;
+            EndRect = endRect;
+            AnimationDuration = animationDuration;
+            Easing = easing;
+        }
+
+        public InputPaneStateEventArgs(InputPaneState newState, Rect? startRect, Rect endRect)
+            : this(newState, startRect, endRect, default, null)
+        {
+        }
+    }
+}

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

@@ -547,6 +547,7 @@ namespace Avalonia.Controls
             ?? new NoopStorageProvider();
 
         public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
+        public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>();
 
         /// <summary>
         /// Gets the platform's clipboard implementation

+ 1 - 1
src/Browser/Avalonia.Browser/AvaloniaView.cs

@@ -70,7 +70,7 @@ namespace Avalonia.Browser
 
             _splash = DomHelper.GetElementById("avalonia-splash");
 
-            _topLevelImpl = new BrowserTopLevelImpl(this);
+            _topLevelImpl = new BrowserTopLevelImpl(this, _containerElement);
 
             _topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
             {

+ 37 - 0
src/Browser/Avalonia.Browser/BrowserInputPane.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Controls.Platform;
+
+namespace Avalonia.Browser;
+
+internal class BrowserInputPane : IInputPane
+{
+    public BrowserInputPane(JSObject container)
+    {
+        InputHelper.SubscribeKeyboardGeometryChange(container, OnGeometryChange);
+    }
+
+    public InputPaneState State { get; private set; }
+    public Rect OccludedRect { get; private set; }
+    public event EventHandler<InputPaneStateEventArgs>? StateChanged;
+    
+    private bool OnGeometryChange(JSObject args)
+    {
+        var oldState = (OccludedRect, State);
+
+        OccludedRect = new Rect(
+            args.GetPropertyAsDouble("x"),
+            args.GetPropertyAsDouble("y"),
+            args.GetPropertyAsDouble("width"),
+            args.GetPropertyAsDouble("height"));
+        State = OccludedRect.Width != 0 ? InputPaneState.Open : InputPaneState.Closed;
+
+        if (oldState != (OccludedRect, State))
+        {
+            StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect));
+        }
+
+        return true;
+    }
+}

+ 9 - 1
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using System.Runtime.InteropServices.JavaScript;
 using System.Runtime.Versioning;
 using Avalonia.Browser.Skia;
 using Avalonia.Browser.Storage;
@@ -33,8 +34,9 @@ namespace Avalonia.Browser
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly ClipboardImpl _clipboard;
         private readonly IInsetsManager? _insetsManager;
+        private readonly IInputPane _inputPane;
 
-        public BrowserTopLevelImpl(AvaloniaView avaloniaView)
+        public BrowserTopLevelImpl(AvaloniaView avaloniaView, JSObject container)
         {
             Surfaces = Enumerable.Empty<object>();
             _avaloniaView = avaloniaView;
@@ -47,6 +49,7 @@ namespace Avalonia.Browser
             _storageProvider = new BrowserStorageProvider();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
             _clipboard = new ClipboardImpl();
+            _inputPane = new BrowserInputPane(container);
         }
 
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
@@ -273,6 +276,11 @@ namespace Avalonia.Browser
             {
                 return _clipboard;
             }
+            
+            if (featureType == typeof(IInputPane))
+            {
+                return _inputPane;
+            }
 
             return null;
         }

+ 4 - 0
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@@ -51,6 +51,10 @@ internal static partial class InputHelper
     public static partial void SubscribeDropEvents(JSObject containerElement,
         [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent);
     
+    [JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)]
+    public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement,
+        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> handler);
+
     [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
     [return: JSMarshalAs<JSType.Array<JSType.Object>>]
     public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);

+ 18 - 0
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@@ -245,6 +245,24 @@ export class InputHelper {
         return pointerEvent.getCoalescedEvents();
     }
 
+    public static subscribeKeyboardGeometryChange(
+        element: HTMLInputElement,
+        handler: (args: any) => boolean) {
+        if ("virtualKeyboard" in navigator) {
+            // (navigator as any).virtualKeyboard.overlaysContent = true;
+            (navigator as any).virtualKeyboard.addEventListener("geometrychange", (event: any) => {
+                const elementRect = element.getBoundingClientRect();
+                const keyboardRect = event.target.boundingRect as DOMRect;
+                handler({
+                    x: keyboardRect.x - elementRect.x,
+                    y: keyboardRect.y - elementRect.y,
+                    width: keyboardRect.width,
+                    height: keyboardRect.height
+                });
+            });
+        }
+    }
+
     public static clearInput(inputElement: HTMLInputElement) {
         inputElement.value = "";
     }

+ 77 - 0
src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs

@@ -0,0 +1,77 @@
+using System;
+using Avalonia.Controls.Platform;
+using Avalonia.MicroCom;
+using Avalonia.Win32.Interop;
+using Avalonia.Win32.Win32Com;
+
+namespace Avalonia.Win32.Input;
+
+internal unsafe class WindowsInputPane : IInputPane, IDisposable
+{
+    // GUID: D5120AA3-46BA-44C5-822D-CA8092C1FC72
+    private static readonly Guid CLSID_FrameworkInputPane = new(0xD5120AA3, 0x46BA, 0x44C5, 0x82, 0x2D, 0xCA, 0x80, 0x92, 0xC1, 0xFC, 0x72);
+    // GUID: 5752238B-24F0-495A-82F1-2FD593056796
+    private static readonly Guid SID_IFrameworkInputPane  = new(0x5752238B, 0x24F0, 0x495A, 0x82, 0xF1, 0x2F, 0xD5, 0x93, 0x05, 0x67, 0x96);
+
+    private readonly WindowImpl _windowImpl;
+    private readonly IFrameworkInputPane _inputPane;
+    private readonly uint _cookie;
+
+    public WindowsInputPane(WindowImpl windowImpl)
+    {
+        _windowImpl = windowImpl;
+        _inputPane = UnmanagedMethods.CreateInstance<IFrameworkInputPane>(in CLSID_FrameworkInputPane, in SID_IFrameworkInputPane);
+
+        using (var handler = new Handler(this))
+        {
+            uint cookie = 0;
+            _inputPane.AdviseWithHWND(windowImpl.Handle.Handle, handler, &cookie);
+            _cookie = cookie;
+        }
+    }
+    public InputPaneState State { get; private set; }
+
+    public Rect OccludedRect { get; private set; }
+
+    public event EventHandler<InputPaneStateEventArgs>? StateChanged;
+
+    private void OnStateChanged(bool showing, UnmanagedMethods.RECT? prcInputPaneScreenLocation)
+    {
+        var oldState = (OccludedRect, State);
+        OccludedRect = prcInputPaneScreenLocation.HasValue
+            ? ScreenRectToClient(prcInputPaneScreenLocation.Value)
+            : default;
+        State = showing ? InputPaneState.Open : InputPaneState.Closed;
+
+        if (oldState != (OccludedRect, State))
+        {
+            StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect));
+        }
+    }
+
+    private Rect ScreenRectToClient(UnmanagedMethods.RECT screenRect)
+    {
+        var position = new PixelPoint(screenRect.left, screenRect.top);
+        var size = new PixelSize(screenRect.Width, screenRect.Height);
+        return new Rect(_windowImpl.PointToClient(position), size.ToSize(_windowImpl.DesktopScaling));
+    }
+
+    public void Dispose()
+    {
+        if (_cookie != 0)
+        {
+            _inputPane.Unadvise(_cookie);
+        }
+
+        _inputPane.Dispose();
+    }
+
+    private class Handler : CallbackBase, IFrameworkInputPaneHandler
+    {
+        private readonly WindowsInputPane _pane;
+
+        public Handler(WindowsInputPane pane) => _pane = pane;
+        public void Showing(UnmanagedMethods.RECT* rect, int _) => _pane.OnStateChanged(true, *rect);
+        public void Hiding(int fEnsureFocusedElementInView) => _pane.OnStateChanged(false, null);
+    }
+}

+ 1 - 4
src/Windows/Avalonia.Win32/Interop/TaskBarList.cs

@@ -19,10 +19,7 @@ namespace Avalonia.Win32.Interop
         {
             if (s_taskBarList == IntPtr.Zero)
             {
-                Guid clsid = ShellIds.TaskBarList;
-                Guid iid = ShellIds.ITaskBarList2;
-
-                int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList);
+                int result = CoCreateInstance(in ShellIds.TaskBarList, IntPtr.Zero, 1, in ShellIds.ITaskBarList2, out s_taskBarList);
 
                 if (s_taskBarList != IntPtr.Zero)
                 {

+ 5 - 9
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1481,18 +1481,14 @@ namespace Avalonia.Win32.Interop
 
         [DllImport("user32.dll", EntryPoint = "SetCursor")]
         internal static extern IntPtr SetCursor(IntPtr hCursor);
-
-        [DllImport("ole32.dll", PreserveSig = true)]
-        internal static extern int CoCreateInstance(ref Guid clsid,
-            IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter);
-
+        
         [DllImport("ole32.dll", PreserveSig = true)]
-        internal static extern int CoCreateInstance(ref Guid clsid,
-            IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter);
+        internal static extern int CoCreateInstance(in Guid clsid,
+            IntPtr ignore1, int ignore2, in Guid iid, [Out] out IntPtr pUnkOuter);
 
-        internal static T CreateInstance<T>(ref Guid clsid, ref Guid iid) where T : IUnknown
+        internal static T CreateInstance<T>(in Guid clsid, in Guid iid) where T : IUnknown
         {
-            var hresult = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out IntPtr pUnk);
+            var hresult = CoCreateInstance(in clsid, IntPtr.Zero, 1, in iid, out IntPtr pUnk);
             if (hresult != 0)
             {
                 throw new COMException("CreateInstance", hresult);

+ 16 - 0
src/Windows/Avalonia.Win32/Win32Com/win32.idl

@@ -312,3 +312,19 @@ interface IDropTarget : IUnknown
         [in] DropEffect* pdwEffect
     );
 }
+
+[uuid(226C537B-1E76-4D9E-A760-33DB29922F18)]
+interface IFrameworkInputPaneHandler : IUnknown
+{
+    HRESULT Showing(RECT* prcInputPaneScreenLocation, boolean fEnsureFocusedElementInView);
+    HRESULT Hiding(boolean fEnsureFocusedElementInView);
+}
+
+[uuid(5752238B-24F0-495A-82F1-2FD593056796)]
+interface IFrameworkInputPane : IUnknown
+{
+    int Advise(IUnknown* pWindow, IFrameworkInputPaneHandler* pHandler, uint* pdwCookie);
+    int AdviseWithHWND(HWND hwnd, IFrameworkInputPaneHandler* pHandler, uint* pdwCookie);
+    int Unadvise(uint dwCookie);
+    int Location(RECT* prcInputPaneScreenLocation);
+}

+ 1 - 1
src/Windows/Avalonia.Win32/Win32StorageProvider.cs

@@ -87,7 +87,7 @@ namespace Avalonia.Win32
                 {
                     var clsid = isOpenFile ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
                     var iid = UnmanagedMethods.ShellIds.IFileDialog;
-                    var frm = UnmanagedMethods.CreateInstance<IFileDialog>(ref clsid, ref iid);
+                    var frm = UnmanagedMethods.CreateInstance<IFileDialog>(in clsid, in iid);
 
                     var options = frm.Options;
                     options |= DefaultDialogOptions;

+ 1 - 0
src/Windows/Avalonia.Win32/WinRT/winrt.idl

@@ -871,3 +871,4 @@ interface IAccessibilitySettings : IInspectable
     [propget] HRESULT HighContrast([out] [retval] boolean* value);
     [propget] HRESULT HighContrastScheme([out] [retval] HSTRING* value);
 }
+

+ 1 - 0
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -110,6 +110,7 @@ namespace Avalonia.Win32
                         }
 
                         _framebuffer.Dispose();
+                        _inputPane?.Dispose();
 
                         //Window doesn't exist anymore
                         _hwnd = IntPtr.Zero;

+ 7 - 1
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -77,6 +77,7 @@ namespace Avalonia.Win32
 
         private readonly Win32NativeControlHost _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
+        private readonly WindowsInputPane? _inputPane;
         private WndProc _wndProcDelegate;
         private string? _className;
         private IntPtr _hwnd;
@@ -164,7 +165,7 @@ namespace Avalonia.Win32
 
             Screen = new ScreenImpl();
             _storageProvider = new Win32StorageProvider(this);
-
+            _inputPane = Win32Platform.WindowsVersion >= PlatformConstants.Windows10 ? new WindowsInputPane(this) : null;
             _nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap);
             _defaultTransparencyLevel = UseRedirectionBitmap ? WindowTransparencyLevel.None : WindowTransparencyLevel.Transparent;
             _transparencyLevel = _defaultTransparencyLevel;
@@ -342,6 +343,11 @@ namespace Avalonia.Win32
             {
                 return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
             }
+            
+            if (featureType == typeof(IInputPane))
+            {
+                return _inputPane;
+            }
 
             return null;
         }

+ 5 - 0
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -227,6 +227,11 @@ namespace Avalonia.iOS
                     return _clipboard;
                 }
 
+                if (featureType == typeof(IInputPane))
+                {
+                    return UIKitInputPane.Instance;
+                }
+
                 return null;
             }
         }

+ 54 - 0
src/iOS/Avalonia.iOS/UIKitInputPane.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls.Platform;
+using Foundation;
+using UIKit;
+
+#nullable enable
+namespace Avalonia.iOS;
+
+internal sealed class UIKitInputPane : IInputPane
+{
+    public static UIKitInputPane Instance { get; } = new();
+    
+    public UIKitInputPane()
+    {
+        NSNotificationCenter
+            .DefaultCenter
+            .AddObserver(UIKeyboard.WillShowNotification, KeyboardUpNotification);
+        NSNotificationCenter
+            .DefaultCenter
+            .AddObserver(UIKeyboard.WillHideNotification, KeyboardDownNotification);
+    }
+
+    public InputPaneState State { get; private set; }
+    public Rect OccludedRect { get; private set; }
+    public event EventHandler<InputPaneStateEventArgs>? StateChanged;
+
+    private void KeyboardDownNotification(NSNotification obj) => RaiseEventFromNotification(false, obj);
+
+    private void KeyboardUpNotification(NSNotification obj) => RaiseEventFromNotification(true, obj);
+
+    private void RaiseEventFromNotification(bool isUp, NSNotification notification)
+    {
+        State = isUp ? InputPaneState.Open : InputPaneState.Closed;
+
+        var startFrame = UIKeyboard.FrameBeginFromNotification(notification);
+        var endFrame = UIKeyboard.FrameEndFromNotification(notification);
+        var duration = UIKeyboard.AnimationDurationFromNotification(notification);
+        var curve = (UIViewAnimationOptions)UIKeyboard.AnimationCurveFromNotification(notification);
+        IEasing? easing =
+            curve.HasFlag(UIViewAnimationOptions.CurveLinear) ? new LinearEasing()
+            : curve.HasFlag(UIViewAnimationOptions.CurveEaseIn) ? new SineEaseIn()
+            : curve.HasFlag(UIViewAnimationOptions.CurveEaseOut) ? new SineEaseOut()
+            : curve.HasFlag(UIViewAnimationOptions.CurveEaseInOut) ? new SineEaseInOut()
+            : null;
+
+        var startRect = new Rect(startFrame.X, startFrame.Y, startFrame.Width, startFrame.Height);
+        OccludedRect = new Rect(endFrame.X, endFrame.Y, endFrame.Width, endFrame.Height);
+
+        StateChanged?.Invoke(this, new InputPaneStateEventArgs(
+            State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing));
+    }
+}