Browse Source

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 <workgroupengineering@users.noreply.github.com>

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

Co-authored-by: workgroupengineering <workgroupengineering@users.noreply.github.com>

* 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 <maxkatz6@outlook.com>
Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Co-authored-by: workgroupengineering <workgroupengineering@users.noreply.github.com>
pull/13775/head
Emmanuel Hansen 2 years ago
committed by GitHub
parent
commit
6d9780c5b2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  2. 20
      samples/SafeAreaDemo/Views/MainView.xaml
  3. 3
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  4. 183
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  5. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  6. 89
      src/Avalonia.Controls/Platform/IInputPane.cs
  7. 1
      src/Avalonia.Controls/TopLevel.cs
  8. 2
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  9. 37
      src/Browser/Avalonia.Browser/BrowserInputPane.cs
  10. 10
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  11. 4
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  12. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  13. 77
      src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs
  14. 5
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  15. 14
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  16. 16
      src/Windows/Avalonia.Win32/Win32Com/win32.idl
  17. 2
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  18. 1
      src/Windows/Avalonia.Win32/WinRT/winrt.idl
  19. 1
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  20. 8
      src/Windows/Avalonia.Win32/WindowImpl.cs
  21. 5
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  22. 54
      src/iOS/Avalonia.iOS/UIKitInputPane.cs

53
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));
}
}
}

20
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>

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

183
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);
}
}
}

2
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
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
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

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

10
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
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
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
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);
}
}

5
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)
{

14
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
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);
}

2
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
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
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;

8
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
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
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));
}
}
Loading…
Cancel
Save