gesture feature initial

This commit is contained in:
Fabian Freund
2026-05-31 11:40:57 +02:00
parent 4aecb8966c
commit 4d4c8a8786
45 changed files with 4729 additions and 5 deletions
@@ -20,6 +20,7 @@ export 'src/domain/services/gecko_engine_settings.dart';
export 'src/domain/services/gecko_event.dart';
export 'src/domain/services/gecko_fetch_service.dart';
export 'src/domain/services/gecko_find_in_page.dart';
export 'src/domain/services/gecko_gesture.dart';
export 'src/domain/services/gecko_history.dart';
export 'src/domain/services/gecko_icon.dart';
export 'src/domain/services/gecko_logging.dart';
@@ -80,6 +81,7 @@ export 'src/pigeons/gecko.g.dart'
GeckoSuggestionType,
GeckoTrackingProtectionApi,
GeoHitResult,
GestureConfig,
HistoryHighlight,
HistoryHighlightWeights,
HistoryMetadata,
@@ -0,0 +1,98 @@
/*
* 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';
/// Service for native touch-gesture recognition.
///
/// Pushes the user's gesture configuration to the native recognizer via
/// [setGestureConfig] and exposes a stream of recognized gesture keys. The
/// native side is purely observational (it never consumes touch input for
/// gestures), so recognized events fire on touch release for multi-stroke
/// gestures only.
///
/// A gesture key is the canonical encoding shared with native: an optional
/// start-position prefix (`L:`/`R:`/`T:`/`B:`/`W:`/`E:`), an optional
/// finger-count prefix (`2:` …), and the dash-joined directions (`U`/`D`/`L`/
/// `R`), e.g. `R:2:D-L` or `D-R`.
class GeckoGestureService extends GeckoGestureEvents {
final GeckoGestureApi _api;
final _recognizedGestureSubject = PublishSubject<String>();
final _gestureProgressSubject = BehaviorSubject<String?>.seeded(null);
/// Stream of recognized gesture keys.
///
/// Emits the canonical key (e.g. `D-R`) each time the native recognizer
/// matches a configured gesture.
Stream<String> get recognizedGestures => _recognizedGestureSubject.stream;
/// Stream of the in-progress stroke for the live feedback overlay.
///
/// Emits the current partial canonical key (e.g. `R:D`) each time a new
/// arrow is drawn, and `null` when the stroke ends (release, cancel or idle
/// timeout). Consumers render the overlay while non-null.
Stream<String?> get gestureProgress => _gestureProgressSubject.stream;
/// Creates a new gesture service.
///
/// Call [setUp] to register the event handlers after construction.
GeckoGestureService({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : _api = GeckoGestureApi(
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 = '',
}) {
GeckoGestureEvents.setUp(
this,
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
}
/// Pushes the gesture-recognition configuration to native.
///
/// Call whenever the user's gesture settings change.
Future<void> setGestureConfig(GestureConfig config) async {
await _api.setGestureConfig(config);
}
// GeckoGestureEvents implementation
@override
void onGestureRecognized(int sequence, String gestureKey) {
_recognizedGestureSubject.addWhenMoreRecent(sequence, null, gestureKey);
}
@override
void onGestureProgress(int sequence, String partialKey) {
_gestureProgressSubject.addWhenMoreRecent(sequence, null, partialKey);
}
@override
void onGestureReset(int sequence) {
_gestureProgressSubject.addWhenMoreRecent(sequence, null, null);
}
/// Disposes the service and closes all streams.
Future<void> dispose() async {
await _recognizedGestureSubject.close();
await _gestureProgressSubject.close();
}
}
@@ -6098,6 +6098,87 @@ class SandboxCaptureEntry {
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
}
/// Configuration for native touch-gesture recognition.
///
/// Pushed from Dart whenever the user's gesture settings change. Native
/// recognition is purely observational: it assembles a canonical stroke key
/// (start-position prefix + finger-count prefix + dash-joined directions, e.g.
/// `R:2:D-L`) and only emits when that key matches an entry in
/// [activeGestureKeys]. Strokes that do not match are ignored, so normal
/// scrolling, tapping and pinch-zoom are never affected.
class GestureConfig {
GestureConfig({
required this.enabled,
required this.strokeSize,
required this.timeoutMs,
required this.maxFingers,
required this.activeGestureKeys,
});
bool enabled;
/// Base stroke length in logical pixels, scaled at runtime by
/// `min(viewWidth, viewHeight) / 320` to match the reference gesture add-on.
int strokeSize;
/// Milliseconds of inactivity after which an in-progress gesture is
/// discarded.
int timeoutMs;
/// Maximum number of simultaneous pointers a gesture may use.
int maxFingers;
/// Canonical keys that currently have an action bound, e.g. `D-R`,
/// `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
/// when an assembled stroke matches one of these.
List<String> activeGestureKeys;
List<Object?> _toList() {
return <Object?>[
enabled,
strokeSize,
timeoutMs,
maxFingers,
activeGestureKeys,
];
}
Object encode() {
return _toList();
}
static GestureConfig decode(Object result) {
result as List<Object?>;
return GestureConfig(
enabled: result[0]! as bool,
strokeSize: result[1]! as int,
timeoutMs: result[2]! as int,
maxFingers: result[3]! as int,
activeGestureKeys: (result[4]! as List<Object?>).cast<String>(),
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! GestureConfig || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(enabled, other.enabled) &&
_deepEquals(strokeSize, other.strokeSize) &&
_deepEquals(timeoutMs, other.timeoutMs) &&
_deepEquals(maxFingers, other.maxFingers) &&
_deepEquals(activeGestureKeys, other.activeGestureKeys);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
@@ -6474,6 +6555,9 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is SandboxCaptureEntry) {
buffer.putUint8(251);
writeValue(buffer, value.encode());
} else if (value is GestureConfig) {
buffer.putUint8(252);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -6769,6 +6853,8 @@ class _PigeonCodec extends StandardMessageCodec {
return PwaManifest.decode(readValue(buffer)!);
case 251:
return SandboxCaptureEntry.decode(readValue(buffer)!);
case 252:
return GestureConfig.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
@@ -12745,3 +12831,153 @@ abstract class SandboxCaptureHostEvents {
}
}
}
/// Dart → Kotlin. Pushes the current gesture-recognition configuration.
class GeckoGestureApi {
/// Constructor for [GeckoGestureApi]. 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.
GeckoGestureApi({
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;
Future<void> setGestureConfig(GestureConfig config) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureApi.setGestureConfig$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[config],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
_extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: true,
);
}
}
/// Kotlin → Dart. Emitted when an assembled touch stroke matches a configured
/// gesture key.
abstract class GeckoGestureEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// [sequence] Event sequence number for ordering.
/// [gestureKey] Canonical key of the recognized gesture, e.g. `D-R`.
void onGestureRecognized(int sequence, String gestureKey);
/// Emitted while a stroke is being drawn, each time a new direction arrow is
/// appended. Drives the live feedback overlay.
///
/// [sequence] Event sequence number for ordering.
/// [partialKey] Current partial canonical key including start/finger
/// prefixes, e.g. `R:D`.
void onGestureProgress(int sequence, String partialKey);
/// Emitted when an in-progress stroke ends (release, cancel or idle timeout)
/// so the live feedback overlay can be hidden.
///
/// [sequence] Event sequence number for ordering.
void onGestureReset(int sequence);
static void setUp(
GeckoGestureEvents? api, {
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureRecognized$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int arg_sequence = args[0]! as int;
final String arg_gestureKey = args[1]! as String;
try {
api.onGestureRecognized(arg_sequence, arg_gestureKey);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
}
});
}
}
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureProgress$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int arg_sequence = args[0]! as int;
final String arg_partialKey = args[1]! as String;
try {
api.onGestureProgress(arg_sequence, arg_partialKey);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
}
});
}
}
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoGestureEvents.onGestureReset$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int arg_sequence = args[0]! as int;
try {
api.onGestureReset(arg_sequence);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
}
});
}
}
}
}