impelement geckoview intent handling via viewport api; fix lints and ui issues

This commit is contained in:
Fabian Freund
2026-01-13 11:03:26 +01:00
parent 5e446f1b92
commit 849823f5df
18 changed files with 960 additions and 33 deletions
@@ -24,6 +24,7 @@ import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActi
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
@@ -90,6 +91,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
// Keyboard visibility detection feature
private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null
// Registers a photo picker activity launcher in single-select mode.
private val singleMediaPicker =
AndroidPhotoPicker.singleMediaPicker(
@@ -220,6 +224,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
components.engineView = engineView
// Apply any pending viewport settings that were set before engineView was ready
GlobalComponents.viewportApi?.applyPendingSettings()
sessionFeature.set(
feature = SessionFeature(
components.core.store,
@@ -468,6 +475,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
components.core.historyStorage.registerStorageMaintenanceWorker()
// Start keyboard visibility detection if viewport events are available
GlobalComponents.viewportEvents?.let { viewportEvents ->
keyboardVisibilityFeature = KeyboardVisibilityFeature(viewportEvents).also {
it.start(binding.root)
}
}
} catch (e: Exception) {
Log.e("EngineCreation", "Failed to create engine: ${e.message}", e)
context?.let { restartApp(it) }
@@ -560,6 +574,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
override fun onDestroyView() {
super.onDestroyView()
// Stop keyboard visibility detection
keyboardVisibilityFeature?.stop()
keyboardVisibilityFeature = null
GlobalComponents.onPullToRefreshEnabledChanged = null
components.engineView?.setActivityContext(null)
_binding = null
@@ -14,7 +14,9 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -45,6 +47,12 @@ object GlobalComponents {
var onPullToRefreshEnabledChanged: ((Boolean) -> Unit)? = null
// Viewport events for keyboard visibility notifications
var viewportEvents: GeckoViewportEvents? = null
// Viewport API for applying pending settings when engineView becomes available
var viewportApi: GeckoViewportApiImpl? = null
@DelicateCoroutinesApi
private fun restoreBrowserState(newComponents: Components) =
GlobalScope.launch(Dispatchers.Main) {
@@ -44,6 +44,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
import eu.weblibre.flutter_mozilla_components.pigeons.LogLevel
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewEvents
@@ -263,6 +265,17 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl())
GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl())
// Viewport API for dynamic toolbar and keyboard handling
val viewportEvents = GeckoViewportEvents(_flutterPluginBinding.binaryMessenger)
val viewportApi = GeckoViewportApiImpl()
GeckoViewportApi.setUp(
_flutterPluginBinding.binaryMessenger,
viewportApi
)
// Store viewport events and API for keyboard feature and pending settings
GlobalComponents.viewportEvents = viewportEvents
GlobalComponents.viewportApi = viewportApi
ReaderViewEvents.setUp(
_flutterPluginBinding.binaryMessenger,
components.events.readerViewEvents
@@ -0,0 +1,93 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components.api
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
import mozilla.components.support.base.log.logger.Logger
/**
* Implementation of GeckoViewportApi that controls GeckoView's viewport behavior
* for dynamic toolbar and keyboard handling.
*
* This allows Flutter to control how GeckoView adjusts its internal viewport
* without resizing the platform view itself, avoiding visual flickering.
*/
class GeckoViewportApiImpl : GeckoViewportApi {
companion object {
private const val TAG = "GeckoViewportApi"
}
private val logger = Logger(TAG)
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
// Store the current dynamic toolbar max height
private var currentDynamicToolbarMaxHeight: Int = 0
/**
* Sets the maximum height that dynamic toolbars (top + bottom) can occupy.
*
* GeckoView will adjust its internal viewport calculations to account for
* this space. The website will receive proper viewport dimensions through
* standard web APIs (CSS viewport units, window.innerHeight).
*/
override fun setDynamicToolbarMaxHeight(heightPx: Long) {
val height = heightPx.toInt()
currentDynamicToolbarMaxHeight = height
val engineView = components.engineView
if (engineView == null) {
logger.warn("$TAG: setDynamicToolbarMaxHeight called but engineView is null")
return
}
logger.debug("$TAG: setDynamicToolbarMaxHeight($height)")
engineView.setDynamicToolbarMaxHeight(height)
}
/**
* Sets the vertical clipping offset for the GeckoView content.
*
* Use this as the toolbar animates to clip content at the bottom.
* Negative values clip from the bottom (for bottom toolbar sliding up).
* Positive values clip from the top (for top toolbar sliding down).
*/
override fun setVerticalClipping(clippingPx: Long) {
val clipping = clippingPx.toInt()
val engineView = components.engineView
if (engineView == null) {
logger.warn("$TAG: setVerticalClipping called but engineView is null")
return
}
logger.debug("$TAG: setVerticalClipping($clipping)")
engineView.setVerticalClipping(clipping)
}
/**
* Applies any pending viewport settings that were set before engineView was available.
*
* Call this method after setting components.engineView to ensure that any
* setDynamicToolbarMaxHeight calls made during startup are properly applied.
*/
fun applyPendingSettings() {
val engineView = components.engineView
if (engineView == null) {
logger.warn("$TAG: applyPendingSettings called but engineView is still null")
return
}
if (currentDynamicToolbarMaxHeight > 0) {
logger.debug("$TAG: Applying pending dynamicToolbarMaxHeight: $currentDynamicToolbarMaxHeight")
engineView.setDynamicToolbarMaxHeight(currentDynamicToolbarMaxHeight)
}
}
}
@@ -0,0 +1,223 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components.feature
import android.os.Build
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowInsets
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
/**
* Feature that detects keyboard visibility changes and reports them to Flutter.
*
* Uses WindowInsets API for accurate keyboard detection on Android 11+ (API 30+),
* with fallback to ViewTreeObserver.OnGlobalLayoutListener for older versions.
*/
class KeyboardVisibilityFeature(
private val flutterEvents: GeckoViewportEvents
) {
companion object {
private const val TAG = "KeyboardVisibilityFeature"
}
private val logger = Logger(TAG)
private var rootView: View? = null
private var lastKeyboardHeight: Int = 0
private var lastKeyboardVisible: Boolean = false
private var isAnimating: Boolean = false
// For legacy keyboard detection (pre-API 30)
private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
// For modern keyboard animation tracking (API 30+)
private var insetsAnimationCallback: WindowInsetsAnimationCompat.Callback? = null
/**
* Start observing keyboard visibility changes on the given root view.
*/
fun start(view: View) {
rootView = view
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setupModernKeyboardDetection(view)
} else {
setupLegacyKeyboardDetection(view)
}
// Also set up WindowInsets listener for immediate state
setupInsetsListener(view)
logger.debug("$TAG: Started keyboard visibility detection")
}
/**
* Stop observing keyboard visibility changes.
*/
fun stop() {
rootView?.let { view ->
globalLayoutListener?.let {
view.viewTreeObserver.removeOnGlobalLayoutListener(it)
}
insetsAnimationCallback?.let {
ViewCompat.setWindowInsetsAnimationCallback(view, null)
}
}
globalLayoutListener = null
insetsAnimationCallback = null
rootView = null
logger.debug("$TAG: Stopped keyboard visibility detection")
}
/**
* Modern keyboard detection using WindowInsetsAnimation (API 30+).
* This provides smooth animation callbacks during keyboard show/hide.
*/
private fun setupModernKeyboardDetection(view: View) {
insetsAnimationCallback = object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
super.onPrepare(animation)
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
isAnimating = true
}
}
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
// Find IME animation
val imeAnimation = runningAnimations.find {
it.typeMask and WindowInsetsCompat.Type.ime() != 0
}
if (imeAnimation != null) {
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
// Keyboard height is IME height minus navigation bar (which is included)
val keyboardHeight = (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0)
// During animation, report height changes
if (keyboardHeight != lastKeyboardHeight) {
notifyKeyboardChange(keyboardHeight, keyboardHeight > 0, isAnimating = true)
}
}
return insets
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
super.onEnd(animation)
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
isAnimating = false
// Send final state
notifyKeyboardChange(lastKeyboardHeight, lastKeyboardVisible, isAnimating = false)
}
}
}
ViewCompat.setWindowInsetsAnimationCallback(view, insetsAnimationCallback)
}
/**
* Legacy keyboard detection using ViewTreeObserver (pre-API 30).
*/
private fun setupLegacyKeyboardDetection(view: View) {
globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
val insets = ViewCompat.getRootWindowInsets(view) ?: return@OnGlobalLayoutListener
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val keyboardHeight = (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0)
val isVisible = keyboardHeight > 0
if (keyboardHeight != lastKeyboardHeight || isVisible != lastKeyboardVisible) {
notifyKeyboardChange(keyboardHeight, isVisible, isAnimating = false)
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}
/**
* Set up WindowInsets listener for immediate keyboard state.
*/
private fun setupInsetsListener(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val keyboardHeight = if (imeVisible) {
(imeInsets.bottom - navInsets.bottom).coerceAtLeast(0)
} else {
0
}
// Only notify if not currently animating (animation callbacks handle that)
if (!isAnimating && (keyboardHeight != lastKeyboardHeight || imeVisible != lastKeyboardVisible)) {
notifyKeyboardChange(keyboardHeight, imeVisible, isAnimating = false)
}
insets
}
}
/**
* Notify Flutter about keyboard visibility change.
*/
private fun notifyKeyboardChange(heightPx: Int, isVisible: Boolean, isAnimating: Boolean) {
lastKeyboardHeight = heightPx
lastKeyboardVisible = isVisible
val timestamp = System.currentTimeMillis()
logger.debug("$TAG: Keyboard change - height=$heightPx, visible=$isVisible, animating=$isAnimating")
runOnUiThread {
flutterEvents.onKeyboardVisibilityChanged(
timestamp,
heightPx.toLong(),
isVisible,
isAnimating
) { /* callback - ignore result */ }
}
}
/**
* Manually trigger a keyboard state check.
* Useful after configuration changes or when resuming.
*/
fun checkKeyboardState() {
rootView?.let { view ->
val insets = ViewCompat.getRootWindowInsets(view) ?: return
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val keyboardHeight = if (imeVisible) {
(imeInsets.bottom - navInsets.bottom).coerceAtLeast(0)
} else {
0
}
notifyKeyboardChange(keyboardHeight, imeVisible, isAnimating = false)
}
}
}
@@ -5995,6 +5995,137 @@ interface GeckoFetchApi {
}
}
}
/**
* Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling.
*
* This API allows Flutter to control how GeckoView adjusts its internal viewport
* without resizing the platform view itself, avoiding visual flickering.
*
* The dynamic toolbar system works by:
* 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight]
* 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping]
* 3. GeckoView internally adjusts viewport and notifies the website
*
* Generated interface from Pigeon that represents a handler of messages from Flutter.
*/
interface GeckoViewportApi {
/**
* Sets the maximum height that dynamic toolbars (top + bottom) can occupy.
*
* GeckoView will adjust its internal viewport calculations to account for
* this space. The website will receive proper viewport dimensions through
* standard web APIs (CSS viewport units, window.innerHeight).
*
* Call this once when toolbar dimensions are known, and again if they change.
*
* [heightPx] Combined height of top and bottom toolbars in pixels.
*/
fun setDynamicToolbarMaxHeight(heightPx: Long)
/**
* Sets the vertical clipping offset for the GeckoView content.
*
* Use this as the toolbar animates to clip content at the bottom.
* Negative values clip from the bottom (for bottom toolbar sliding up).
* Positive values clip from the top (for top toolbar sliding down).
*
* Call this during toolbar animation frames to smoothly adjust the visible area.
*
* [clippingPx] The clipping offset in pixels. Negative = bottom clip.
*/
fun setVerticalClipping(clippingPx: Long)
companion object {
/** The codec used by GeckoViewportApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoViewportApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoViewportApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val heightPxArg = args[0] as Long
val wrapped: List<Any?> = try {
api.setDynamicToolbarMaxHeight(heightPxArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val clippingPxArg = args[0] as Long
val wrapped: List<Any?> = try {
api.setVerticalClipping(clippingPxArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/**
* Events from native side about viewport and input-related changes.
*
* These events allow Flutter to react to native viewport changes,
* particularly keyboard visibility which is detected natively.
*
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
*/
class GeckoViewportEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoViewportEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
/**
* Called when keyboard visibility changes.
*
* This is detected natively using WindowInsets API and provides
* accurate keyboard height information.
*
* [timestamp] Event timestamp for ordering.
* [heightPx] Keyboard height in pixels (0 when hidden).
* [isVisible] Whether the keyboard is currently visible.
* [isAnimating] Whether the keyboard is currently animating.
*/
fun onKeyboardVisibilityChanged(timestampArg: Long, heightPxArg: Long, isVisibleArg: Boolean, isAnimatingArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, heightPxArg, isVisibleArg, isAnimatingArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
}
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoBookmarksApi {
/**
@@ -30,6 +30,7 @@ export 'src/domain/services/gecko_session.dart';
export 'src/domain/services/gecko_suggestions.dart';
export 'src/domain/services/gecko_tab.dart';
export 'src/domain/services/gecko_tab_content.dart';
export 'src/domain/services/gecko_viewport.dart';
export 'src/geckoview_widget.dart';
export 'src/pigeons/gecko.g.dart'
show
@@ -0,0 +1,128 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
import 'package:rxdart/rxdart.dart';
/// Keyboard visibility event data
typedef KeyboardEvent = ({
int heightPx,
bool isVisible,
bool isAnimating,
});
/// Service for controlling GeckoView's viewport behavior.
///
/// This service provides:
/// - Dynamic toolbar height management (setDynamicToolbarMaxHeight)
/// - Vertical clipping control (setVerticalClipping)
/// - Keyboard visibility events from native
///
/// Use this service to implement Firefox-style dynamic toolbars that adjust
/// the GeckoView viewport without resizing the Flutter platform view.
class GeckoViewportService extends GeckoViewportEvents {
final GeckoViewportApi _api;
// Stream controllers for events from native
final _keyboardSubject = BehaviorSubject<KeyboardEvent>.seeded((
heightPx: 0,
isVisible: false,
isAnimating: false,
));
/// Stream of keyboard visibility changes.
///
/// Emits events whenever the soft keyboard shows/hides.
/// The [KeyboardEvent] includes:
/// - [heightPx]: Keyboard height in pixels (0 when hidden)
/// - [isVisible]: Whether keyboard is currently visible
/// - [isAnimating]: Whether keyboard is currently animating
ValueStream<KeyboardEvent> get keyboardEvents => _keyboardSubject.stream;
/// Current keyboard height in pixels.
int get currentKeyboardHeight => _keyboardSubject.value.heightPx;
/// Whether the keyboard is currently visible.
bool get isKeyboardVisible => _keyboardSubject.value.isVisible;
/// Creates a new viewport service.
///
/// Call [setUp] to register the event handlers after construction.
GeckoViewportService({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : _api = GeckoViewportApi(
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
/// Sets up the service to receive events from native.
///
/// Must be called before events will be received.
void setUp({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
GeckoViewportEvents.setUp(
this,
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
}
/// Sets the maximum height that dynamic toolbars can occupy.
///
/// GeckoView will adjust its internal viewport calculations to account for
/// this space. The website will receive proper viewport dimensions through
/// standard web APIs (CSS viewport units, window.innerHeight).
///
/// Call this once when toolbar dimensions are known, and again if they change.
///
/// [heightPx] Combined height of top and bottom toolbars in pixels.
Future<void> setDynamicToolbarMaxHeight(int heightPx) async {
await _api.setDynamicToolbarMaxHeight(heightPx);
}
/// Sets the vertical clipping offset for GeckoView content.
///
/// Use this as the toolbar animates to clip content at the bottom.
/// Negative values clip from the bottom (for bottom toolbar sliding up).
/// Positive values clip from the top (for top toolbar sliding down).
///
/// Call this during toolbar animation frames to smoothly adjust the visible area.
///
/// [clippingPx] The clipping offset in pixels. Negative = bottom clip.
Future<void> setVerticalClipping(int clippingPx) async {
await _api.setVerticalClipping(clippingPx);
}
// GeckoViewportEvents implementation
@override
void onKeyboardVisibilityChanged(
int timestamp,
int heightPx,
bool isVisible,
bool isAnimating,
) {
_keyboardSubject.addWhenMoreRecent(
timestamp,
null,
(
heightPx: heightPx,
isVisible: isVisible,
isAnimating: isAnimating,
),
);
}
/// Disposes the service and closes all streams.
Future<void> dispose() async {
await _keyboardSubject.close();
}
}
@@ -7121,6 +7121,148 @@ class GeckoFetchApi {
}
}
/// Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling.
///
/// This API allows Flutter to control how GeckoView adjusts its internal viewport
/// without resizing the platform view itself, avoiding visual flickering.
///
/// The dynamic toolbar system works by:
/// 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight]
/// 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping]
/// 3. GeckoView internally adjusts viewport and notifies the website
class GeckoViewportApi {
/// Constructor for [GeckoViewportApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
GeckoViewportApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// Sets the maximum height that dynamic toolbars (top + bottom) can occupy.
///
/// GeckoView will adjust its internal viewport calculations to account for
/// this space. The website will receive proper viewport dimensions through
/// standard web APIs (CSS viewport units, window.innerHeight).
///
/// Call this once when toolbar dimensions are known, and again if they change.
///
/// [heightPx] Combined height of top and bottom toolbars in pixels.
Future<void> setDynamicToolbarMaxHeight(int heightPx) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[heightPx]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/// Sets the vertical clipping offset for the GeckoView content.
///
/// Use this as the toolbar animates to clip content at the bottom.
/// Negative values clip from the bottom (for bottom toolbar sliding up).
/// Positive values clip from the top (for top toolbar sliding down).
///
/// Call this during toolbar animation frames to smoothly adjust the visible area.
///
/// [clippingPx] The clipping offset in pixels. Negative = bottom clip.
Future<void> setVerticalClipping(int clippingPx) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[clippingPx]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}
/// Events from native side about viewport and input-related changes.
///
/// These events allow Flutter to react to native viewport changes,
/// particularly keyboard visibility which is detected natively.
abstract class GeckoViewportEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// Called when keyboard visibility changes.
///
/// This is detected natively using WindowInsets API and provides
/// accurate keyboard height information.
///
/// [timestamp] Event timestamp for ordering.
/// [heightPx] Keyboard height in pixels (0 when hidden).
/// [isVisible] Whether the keyboard is currently visible.
/// [isAnimating] Whether the keyboard is currently animating.
void onKeyboardVisibilityChanged(int timestamp, int heightPx, bool isVisible, bool isAnimating);
static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null int.');
final int? arg_heightPx = (args[1] as int?);
assert(arg_heightPx != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null int.');
final bool? arg_isVisible = (args[2] as bool?);
assert(arg_isVisible != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null bool.');
final bool? arg_isAnimating = (args[3] as bool?);
assert(arg_isAnimating != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null bool.');
try {
api.onKeyboardVisibilityChanged(arg_timestamp!, arg_heightPx!, arg_isVisible!, arg_isAnimating!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
class GeckoBookmarksApi {
/// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
@@ -1563,6 +1563,71 @@ class BookmarkInfo {
});
}
// =============================================================================
// Viewport & Dynamic Toolbar APIs
// =============================================================================
/// Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling.
///
/// This API allows Flutter to control how GeckoView adjusts its internal viewport
/// without resizing the platform view itself, avoiding visual flickering.
///
/// The dynamic toolbar system works by:
/// 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight]
/// 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping]
/// 3. GeckoView internally adjusts viewport and notifies the website
@HostApi()
abstract class GeckoViewportApi {
/// Sets the maximum height that dynamic toolbars (top + bottom) can occupy.
///
/// GeckoView will adjust its internal viewport calculations to account for
/// this space. The website will receive proper viewport dimensions through
/// standard web APIs (CSS viewport units, window.innerHeight).
///
/// Call this once when toolbar dimensions are known, and again if they change.
///
/// [heightPx] Combined height of top and bottom toolbars in pixels.
void setDynamicToolbarMaxHeight(int heightPx);
/// Sets the vertical clipping offset for the GeckoView content.
///
/// Use this as the toolbar animates to clip content at the bottom.
/// Negative values clip from the bottom (for bottom toolbar sliding up).
/// Positive values clip from the top (for top toolbar sliding down).
///
/// Call this during toolbar animation frames to smoothly adjust the visible area.
///
/// [clippingPx] The clipping offset in pixels. Negative = bottom clip.
void setVerticalClipping(int clippingPx);
}
/// Events from native side about viewport and input-related changes.
///
/// These events allow Flutter to react to native viewport changes,
/// particularly keyboard visibility which is detected natively.
@FlutterApi()
abstract class GeckoViewportEvents {
/// Called when keyboard visibility changes.
///
/// This is detected natively using WindowInsets API and provides
/// accurate keyboard height information.
///
/// [timestamp] Event timestamp for ordering.
/// [heightPx] Keyboard height in pixels (0 when hidden).
/// [isVisible] Whether the keyboard is currently visible.
/// [isAnimating] Whether the keyboard is currently animating.
void onKeyboardVisibilityChanged(
int timestamp,
int heightPx,
bool isVisible,
bool isAnimating,
);
}
// =============================================================================
// Bookmarks API
// =============================================================================
@HostApi()
abstract class GeckoBookmarksApi {
/// Produces a bookmarks tree for the given guid string.