impelement geckoview intent handling via viewport api; fix lints and ui issues
This commit is contained in:
+18
@@ -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
|
||||
|
||||
+8
@@ -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) {
|
||||
|
||||
+13
@@ -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
|
||||
|
||||
+93
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+223
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user