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
@@ -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();
}
}