Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ protected override void Initialize()
ApiExtensibility.Register<MediaPlayer>(typeof(IMediaPlayerExtension), o => new BrowserMediaPlayerExtension(o));
ApiExtensibility.Register<MediaPlayerPresenter>(typeof(IMediaPlayerPresenterExtension), o => new BrowserMediaPlayerPresenterExtension(o));
ApiExtensibility.Register<CoreWebView2>(typeof(INativeWebViewProvider), o => new BrowserWebViewProvider(o));
ApiExtensibility.Register<Windows.UI.ViewManagement.InputPane>(
typeof(Windows.UI.ViewManagement.IInputPaneExtension),
_ => new Uno.WinUI.Runtime.Skia.WebAssembly.InputPaneExtension());

NativeMethods.PersistBootstrapperLoader();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Windows.UI.ViewManagement;

namespace Uno.WinUI.Runtime.Skia.WebAssembly;

/// <summary>
/// WebAssembly implementation of InputPane functionality for tracking soft keyboard visibility.
/// </summary>
/// <remarks>
/// This implementation uses the Visual Viewport API to detect when the soft keyboard appears
/// on mobile browsers. When the keyboard is shown:
///
/// 1. The visualViewport.height decreases to reflect the visible area (excluding keyboard)
/// 2. The TypeScript side (InputPaneExtension.ts) monitors these changes
/// 3. When a significant height change is detected (>100px), it's considered a keyboard event
/// 4. The C# InputPane is updated with the occluded rectangle representing the keyboard area
/// 5. WebAssemblyWindowWrapper.ts uses visualViewport dimensions for window sizing
///
/// This ensures that:
/// - The app layout uses only the visible viewport area (above the keyboard)
/// - ScrollViewers can properly scroll content into view
/// - Dialogs and other UI elements fit within the available space
/// - Content is not cut off by the soft keyboard
///
/// The implementation is compatible with iOS Safari, Chrome on Android, and other modern mobile browsers
/// that support the Visual Viewport API. For browsers without this API, it falls back to tracking
/// window.innerHeight changes.
/// </remarks>
internal partial class InputPaneExtension : IInputPaneExtension
{
private static InputPaneExtension? _instance;

public InputPaneExtension()
{
_instance = this;

Check warning on line 36 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/ViewManagement/InputPaneExtension.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/ViewManagement/InputPaneExtension.cs#L36

Remove this assignment of '_instance' or initialize it statically.
NativeMethods.Initialize(this);
}

public bool TryShow()
{
// In browsers, the keyboard is shown automatically when an input is focused
// We don't need to explicitly show it
return true;
}

public bool TryHide()
{
// In browsers, we can blur the active element to hide the keyboard
NativeMethods.HideKeyboard();
return true;
}

[JSExport]
private static void OnKeyboardVisibilityChanged(bool visible, double occludedHeight)
{
if (_instance is null)
{
return;
}

var inputPane = InputPane.GetForCurrentView();
var windowWrapper = Uno.UI.Runtime.Skia.WebAssemblyWindowWrapper.Instance;

if (visible && occludedHeight > 0)
{
// Calculate the occluded rect based on the keyboard height
var bounds = windowWrapper.Bounds;
var occludedRect = new Windows.Foundation.Rect(
0,
bounds.Height - occludedHeight,
bounds.Width,
occludedHeight
);
inputPane.OccludedRect = occludedRect;
}
else
{
// No occlusion when keyboard is hidden
inputPane.OccludedRect = new Windows.Foundation.Rect(0, 0, 0, 0);
}
}

private static partial class NativeMethods
{
[JSImport("globalThis.Uno.UI.Runtime.Skia.InputPaneExtension.initialize")]
public static partial void Initialize([JSMarshalAs<JSType.Any>] object instance);

[JSImport("globalThis.Uno.UI.Runtime.Skia.InputPaneExtension.hideKeyboard")]
public static partial void HideKeyboard();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
namespace Uno.UI.Runtime.Skia {
export class InputPaneExtension {
private static _exports: any;
private static _instance: InputPaneExtension | null = null;
private _managedInstance: any;
private _lastViewportHeight: number = 0;

Check notice on line 6 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L6

Type number trivially inferred from a number literal, remove type annotation
private _isKeyboardVisible: boolean = false;

Check notice on line 7 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L7

Type boolean trivially inferred from a boolean literal, remove type annotation

// Threshold for considering keyboard visible (in pixels)
private static readonly KEYBOARD_THRESHOLD_PX = 100;
// Minimum viewport height change to trigger an update (in pixels)
private static readonly MIN_HEIGHT_CHANGE_PX = 1;

public static async initialize(managedInstance: any): Promise<void> {
const module = <any>window.Module;
if (InputPaneExtension._exports === undefined && module.getAssemblyExports !== undefined) {
const browserExports = await module.getAssemblyExports("Uno.UI.Runtime.Skia.WebAssembly.Browser");
InputPaneExtension._exports = browserExports.Uno.WinUI.Runtime.Skia.WebAssembly.InputPaneExtension;
}

if (InputPaneExtension._instance === null) {
InputPaneExtension._instance = new InputPaneExtension(managedInstance);
}
}

private constructor(managedInstance: any) {
this._managedInstance = managedInstance;
this.setupVisualViewportListeners();
}

private setupVisualViewportListeners(): void {
// Use visualViewport API if available (modern browsers, mobile Safari, Chrome)
if (window.visualViewport) {
this._lastViewportHeight = window.visualViewport.height;

window.visualViewport.addEventListener("resize", () => this.onVisualViewportResize());
window.visualViewport.addEventListener("scroll", () => this.onVisualViewportScroll());
} else {
// Fallback for older browsers - track window resize
this._lastViewportHeight = window.innerHeight;
window.addEventListener("resize", () => this.onWindowResize());
}
}

private onVisualViewportResize(): void {
if (!window.visualViewport) {
return;
}

const visualViewport = window.visualViewport;
const viewportHeight = visualViewport.height;
const windowHeight = window.innerHeight;

// Calculate the keyboard height
// The visual viewport height decreases when the keyboard appears
const keyboardHeight = windowHeight - viewportHeight;

// Consider keyboard visible if it occludes more than the threshold
// This helps avoid false positives from small browser UI changes
const isKeyboardVisible = keyboardHeight > InputPaneExtension.KEYBOARD_THRESHOLD_PX;

if (isKeyboardVisible !== this._isKeyboardVisible ||
Math.abs(viewportHeight - this._lastViewportHeight) > InputPaneExtension.MIN_HEIGHT_CHANGE_PX) {

this._isKeyboardVisible = isKeyboardVisible;
this._lastViewportHeight = viewportHeight;

// Notify managed code about keyboard visibility change
if (InputPaneExtension._exports) {
InputPaneExtension._exports.OnKeyboardVisibilityChanged(
isKeyboardVisible,
isKeyboardVisible ? keyboardHeight : 0

Check notice on line 72 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L72

Missing trailing comma
);
}
}
}

private onVisualViewportScroll(): void {
// When the virtual keyboard appears on some devices,
// the viewport may scroll. We should handle this as well.
this.onVisualViewportResize();
}

private onWindowResize(): void {
// Fallback handler for browsers without visualViewport API
const currentHeight = window.innerHeight;

if (this._lastViewportHeight === 0) {
this._lastViewportHeight = currentHeight;
return;
}

const heightDiff = this._lastViewportHeight - currentHeight;
const isKeyboardVisible = heightDiff > InputPaneExtension.KEYBOARD_THRESHOLD_PX;

if (isKeyboardVisible !== this._isKeyboardVisible) {
this._isKeyboardVisible = isKeyboardVisible;

// Update last height only when keyboard state changes
if (!isKeyboardVisible) {
this._lastViewportHeight = currentHeight;
}

if (InputPaneExtension._exports) {
InputPaneExtension._exports.OnKeyboardVisibilityChanged(
isKeyboardVisible,
isKeyboardVisible ? heightDiff : 0

Check notice on line 107 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L107

Missing trailing comma
);
}
}
}

public static hideKeyboard(): void {
// Blur the active element to hide the keyboard
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}
}

// Expose InputPaneExtension methods to globalThis for C# JSImport access
if (globalThis.Uno === undefined) {
globalThis.Uno = {} as any;
}
if (globalThis.Uno.UI === undefined) {
globalThis.Uno.UI = {} as any;
}
if (globalThis.Uno.UI.Runtime === undefined) {
globalThis.Uno.UI.Runtime = {} as any;
}
if (globalThis.Uno.UI.Runtime.Skia === undefined) {
globalThis.Uno.UI.Runtime.Skia = {} as any;
}

(globalThis.Uno.UI.Runtime.Skia as any).InputPaneExtension = {
initialize: Uno.UI.Runtime.Skia.InputPaneExtension.initialize,
hideKeyboard: Uno.UI.Runtime.Skia.InputPaneExtension.hideKeyboard

Check notice on line 138 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L138

Missing trailing comma

Check notice on line 138 in src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/InputPaneExtension.ts#L138

The key 'hideKeyboard' is not sorted alphabetically
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ namespace Uno.UI.Runtime.Skia {

await Accessibility.setup();

window.addEventListener("resize", x => this.resize());
// Use visualViewport events when available (for soft keyboard support on mobile)
if (window.visualViewport) {
window.visualViewport.addEventListener("resize", x => this.resize());
window.visualViewport.addEventListener("scroll", x => this.resize());
} else {
// Fallback for browsers without visualViewport API
window.addEventListener("resize", x => this.resize());
}

window.addEventListener("contextmenu", x => {
x.preventDefault();
Expand Down Expand Up @@ -103,8 +110,22 @@ namespace Uno.UI.Runtime.Skia {
}

private resize() {
var rect = document.documentElement.getBoundingClientRect();
this.onResize(this.owner, rect.width, rect.height, globalThis.devicePixelRatio);
// Use visualViewport when available (for mobile soft keyboard support)
// visualViewport gives us the actual visible area, excluding soft keyboard
let width: number;
let height: number;

if (window.visualViewport) {
width = window.visualViewport.width;
height = window.visualViewport.height;
} else {
// Fallback for browsers without visualViewport API
var rect = document.documentElement.getBoundingClientRect();
width = rect.width;
height = rect.height;
}

this.onResize(this.owner, width, height, globalThis.devicePixelRatio);
}

public static setCursor(cssCursor: string) {
Expand Down
Loading
Loading