diff --git a/app/lib/features/geckoview/domain/entities/states/tab.dart b/app/lib/features/geckoview/domain/entities/states/tab.dart index d8c30b38..fb498d90 100644 --- a/app/lib/features/geckoview/domain/entities/states/tab.dart +++ b/app/lib/features/geckoview/domain/entities/states/tab.dart @@ -64,6 +64,7 @@ class TabState extends WebPageInfo { final bool isPrivate; final bool isFullScreen; final bool isLoading; + final bool showToolbarAsExpanded; bool get isFinishedLoading => !isLoading && progress == 100; @@ -84,6 +85,7 @@ class TabState extends WebPageInfo { required this.isPrivate, required this.isFullScreen, required this.isLoading, + required this.showToolbarAsExpanded, required this.securityInfoState, required this.historyState, required this.readerableState, @@ -102,6 +104,7 @@ class TabState extends WebPageInfo { isPrivate: false, isFullScreen: false, isLoading: false, + showToolbarAsExpanded: false, securityInfoState: SecurityState.$default(), historyState: HistoryState.$default(), readerableState: ReaderableState.$default(), @@ -120,6 +123,7 @@ class TabState extends WebPageInfo { isPrivate, isFullScreen, isLoading, + showToolbarAsExpanded, securityInfoState, historyState, readerableState, diff --git a/app/lib/features/geckoview/domain/entities/states/tab.g.dart b/app/lib/features/geckoview/domain/entities/states/tab.g.dart index 023199a2..11669ffa 100644 --- a/app/lib/features/geckoview/domain/entities/states/tab.g.dart +++ b/app/lib/features/geckoview/domain/entities/states/tab.g.dart @@ -27,6 +27,8 @@ abstract class _$TabStateCWProxy { TabState isLoading(bool isLoading); + TabState showToolbarAsExpanded(bool showToolbarAsExpanded); + TabState securityInfoState(SecurityState securityInfoState); TabState historyState(HistoryState historyState); @@ -53,6 +55,7 @@ abstract class _$TabStateCWProxy { bool isPrivate, bool isFullScreen, bool isLoading, + bool showToolbarAsExpanded, SecurityState securityInfoState, HistoryState historyState, ReaderableState readerableState, @@ -97,6 +100,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { @override TabState isLoading(bool isLoading) => call(isLoading: isLoading); + @override + TabState showToolbarAsExpanded(bool showToolbarAsExpanded) => + call(showToolbarAsExpanded: showToolbarAsExpanded); + @override TabState securityInfoState(SecurityState securityInfoState) => call(securityInfoState: securityInfoState); @@ -132,6 +139,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { Object? isPrivate = const $CopyWithPlaceholder(), Object? isFullScreen = const $CopyWithPlaceholder(), Object? isLoading = const $CopyWithPlaceholder(), + Object? showToolbarAsExpanded = const $CopyWithPlaceholder(), Object? securityInfoState = const $CopyWithPlaceholder(), Object? historyState = const $CopyWithPlaceholder(), Object? readerableState = const $CopyWithPlaceholder(), @@ -180,6 +188,12 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { ? _value.isLoading // ignore: cast_nullable_to_non_nullable : isLoading as bool, + showToolbarAsExpanded: + showToolbarAsExpanded == const $CopyWithPlaceholder() || + showToolbarAsExpanded == null + ? _value.showToolbarAsExpanded + // ignore: cast_nullable_to_non_nullable + : showToolbarAsExpanded as bool, securityInfoState: securityInfoState == const $CopyWithPlaceholder() || securityInfoState == null diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index 43bce19a..1181e0c2 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -87,6 +87,7 @@ class TabStates extends _$TabStates { isPrivate: contentState.isPrivate, isFullScreen: contentState.isFullScreen, isLoading: contentState.isLoading, + showToolbarAsExpanded: contentState.showToolbarAsExpanded, ); _updateState({...state}..[contentState.id] = newState); diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index d957e5bb..065d40ff 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -41,7 +41,7 @@ final class TabStatesProvider } } -String _$tabStatesHash() => r'985d2eb87c59c3a113b8bbcf9d96267c42a2f543'; +String _$tabStatesHash() => r'975de1242f42e2017754dc560125788b83ad8b72'; abstract class _$TabStates extends $Notifier> { Map build(); diff --git a/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart b/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart deleted file mode 100644 index ae5091ba..00000000 --- a/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024-2026 Fabian Freund. - * - * This file is part of WebLibre - * (see https://weblibre.eu). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'tab_bar_dismissable.g.dart'; - -@Riverpod(keepAlive: true) -class TabBarDismissableController extends _$TabBarDismissableController { - void show() { - state = false; - } - - void dismiss() { - if (!state) { - state = true; - } - } - - @override - bool build() { - return false; - } -} diff --git a/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.g.dart b/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.g.dart deleted file mode 100644 index 02600525..00000000 --- a/app/lib/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.g.dart +++ /dev/null @@ -1,64 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'tab_bar_dismissable.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(TabBarDismissableController) -final tabBarDismissableControllerProvider = - TabBarDismissableControllerProvider._(); - -final class TabBarDismissableControllerProvider - extends $NotifierProvider { - TabBarDismissableControllerProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'tabBarDismissableControllerProvider', - isAutoDispose: false, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$tabBarDismissableControllerHash(); - - @$internal - @override - TabBarDismissableController create() => TabBarDismissableController(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(bool value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$tabBarDismissableControllerHash() => - r'e0c2f700ccebc35bc8029370cdebd63e98bf95e1'; - -abstract class _$TabBarDismissableController extends $Notifier { - bool build(); - @$mustCallSuper - @override - void runBuild() { - final ref = this.ref as $Ref; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier, - bool, - Object?, - Object? - >; - element.handleCreate(ref, build); - } -} diff --git a/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart b/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart new file mode 100644 index 00000000..fefe9d34 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/domain/providers.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; + +part 'toolbar_visibility.g.dart'; + +enum ToolbarVisibility { visible, hidden, dismissed } + +@Riverpod(keepAlive: true) +class ToolbarVisibilityController extends _$ToolbarVisibilityController { + @override + ToolbarVisibility build(String? tabId) { + // Show toolbar when loading starts + ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.isLoading), ( + previous, + next, + ) { + if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) { + return; + } + if (next == true) { + show(); + } + }); + + // Show toolbar on navigation (history state change) + ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.historyState), ( + previous, + next, + ) { + if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) { + return; + } + if (next != null && previous != null && previous != next) { + show(); + } + }); + + // Force-show when GeckoView requests toolbar expansion + // (e.g. touch on form input) + ref.listen( + tabStatesProvider.select( + (tabs) => tabs[tabId]?.showToolbarAsExpanded ?? false, + ), + (previous, next) { + if (next && previous != next) { + forceShow(); + } + }, + ); + + return ToolbarVisibility.visible; + } + + /// Hide toolbar via scroll. All guards checked internally. + void requestHide() { + final settings = ref.read(generalSettingsWithDefaultsProvider); + if (!settings.autoHideTabBar) return; + + final isLoading = ref.read(tabStatesProvider)[tabId]?.isLoading ?? false; + if (isLoading) return; + + final viewportService = ref.read(viewportServiceProvider); + if (!viewportService.isBrowserHandlingScrollEnabled) return; + + if (state == ToolbarVisibility.dismissed) return; + + state = ToolbarVisibility.hidden; + } + + /// Show toolbar (scroll-up, tab change, loading start, etc). + /// Won't show if manually dismissed — use forceShow() for that. + void show() { + if (state == ToolbarVisibility.dismissed) return; + state = ToolbarVisibility.visible; + } + + /// Force-show unconditionally + un-dismiss. + void forceShow() { + state = ToolbarVisibility.visible; + } + + /// Dismiss toolbar (user swipe). Only affects this tab. + void dismiss() { + state = ToolbarVisibility.dismissed; + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.g.dart b/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.g.dart new file mode 100644 index 00000000..8ef1a732 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.g.dart @@ -0,0 +1,111 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'toolbar_visibility.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ToolbarVisibilityController) +final toolbarVisibilityControllerProvider = + ToolbarVisibilityControllerFamily._(); + +final class ToolbarVisibilityControllerProvider + extends $NotifierProvider { + ToolbarVisibilityControllerProvider._({ + required ToolbarVisibilityControllerFamily super.from, + required String? super.argument, + }) : super( + retry: null, + name: r'toolbarVisibilityControllerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$toolbarVisibilityControllerHash(); + + @override + String toString() { + return r'toolbarVisibilityControllerProvider' + '' + '($argument)'; + } + + @$internal + @override + ToolbarVisibilityController create() => ToolbarVisibilityController(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ToolbarVisibility value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is ToolbarVisibilityControllerProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$toolbarVisibilityControllerHash() => + r'242449893afd6780d977544659e83d4641c8a0ad'; + +final class ToolbarVisibilityControllerFamily extends $Family + with + $ClassFamilyOverride< + ToolbarVisibilityController, + ToolbarVisibility, + ToolbarVisibility, + ToolbarVisibility, + String? + > { + ToolbarVisibilityControllerFamily._() + : super( + retry: null, + name: r'toolbarVisibilityControllerProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + ToolbarVisibilityControllerProvider call(String? tabId) => + ToolbarVisibilityControllerProvider._(argument: tabId, from: this); + + @override + String toString() => r'toolbarVisibilityControllerProvider'; +} + +abstract class _$ToolbarVisibilityController + extends $Notifier { + late final _$args = ref.$arg as String?; + String? get tabId => _$args; + + ToolbarVisibility build(String? tabId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ToolbarVisibility, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 90ffe22f..fa6bc491 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -39,7 +39,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart'; @@ -136,7 +136,6 @@ class _TabBar extends HookConsumerWidget { final bool showMainToolbar; final bool showContextualToolbar; final bool showQuickTabSwitcherBar; - final ValueNotifier displayAppBar; final Stream? pointerMoveEvents; final TabBarPosition tabBarPosition; @@ -144,7 +143,6 @@ class _TabBar extends HookConsumerWidget { required this.showMainToolbar, required this.showContextualToolbar, required this.showQuickTabSwitcherBar, - required this.displayAppBar, required this.tabBarPosition, required this.pointerMoveEvents, }); @@ -154,30 +152,34 @@ class _TabBar extends HookConsumerWidget { final tabId = ref.watch(selectedTabProvider); final displayedSheet = ref.watch(bottomSheetControllerProvider); - final autoHideTabBar = ref.watch( - generalSettingsWithDefaultsProvider.select( - (value) => value.autoHideTabBar, - ), - ); - // Auto-hide scroll detection hooks (run unconditionally per hook rules) final diffAcc = useRef(0.0); - void resetHiddenState() { - if (!ref.read(tabBarDismissableControllerProvider)) { - displayAppBar.value = true; + // Reset scroll accumulator on tab switch + useEffect(() { + diffAcc.value = 0.0; + return null; + }, [tabId]); + + // Reset scroll accumulator when controller shows toolbar + // (covers: loading start, navigation, showToolbarAsExpanded, forceShow) + ref.listen(toolbarVisibilityControllerProvider(tabId), (previous, next) { + if (next == ToolbarVisibility.visible && previous != next) { + diffAcc.value = 0.0; } + }); + + void resetHiddenState() { + ref + .read( + toolbarVisibilityControllerProvider( + ref.read(selectedTabProvider), + ).notifier, + ) + .show(); diffAcc.value = 0.0; } - useEffect(() { - if (!autoHideTabBar) return null; - WidgetsBinding.instance.addPostFrameCallback((_) { - resetHiddenState(); - }); - return null; - }, [tabId, autoHideTabBar]); - useOnAppLifecycleStateChange((previous, current) { if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return; if (current == AppLifecycleState.resumed) { @@ -185,38 +187,13 @@ class _TabBar extends HookConsumerWidget { } }); - ref.listen(tabStateProvider(tabId).select((value) => value?.isLoading), ( - previous, - next, - ) { - if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return; - if (next == true) { - resetHiddenState(); - } - }); - - ref.listen(tabStateProvider(tabId).select((value) => value?.historyState), ( - previous, - next, - ) { - if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return; - if (next != null && previous != null) { - if (previous != next) { - resetHiddenState(); - } - } - }); - useOnStreamChange( pointerMoveEvents, onData: (event) { - if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) { - return; - } - // Don't apply auto-hide when toolbar is manually dismissed - if (ref.read(tabBarDismissableControllerProvider)) { - return; - } + final autoHide = ref + .read(generalSettingsWithDefaultsProvider) + .autoHideTabBar; + if (!autoHide) return; final diff = event.dy; if (diff < 0) { @@ -225,7 +202,13 @@ class _TabBar extends HookConsumerWidget { } diffAcc.value += diff; if (diffAcc.value.abs() > kToolbarHeight * 1.5) { - displayAppBar.value = false; + ref + .read( + toolbarVisibilityControllerProvider( + ref.read(selectedTabProvider), + ).notifier, + ) + .requestHide(); } } else if (diff > 0) { if (diffAcc.value < 0) { @@ -267,6 +250,9 @@ class BrowserScreen extends HookConsumerWidget { final tabInFullScreen = ref.watch( selectedTabStateProvider.select((value) => value?.isFullScreen ?? false), ); + final tabIsLoading = ref.watch( + selectedTabStateProvider.select((value) => value?.isLoading ?? false), + ); final overlayController = useOverlayPortalController(); @@ -300,12 +286,6 @@ class BrowserScreen extends HookConsumerWidget { ), ); - final displayAppBar = useValueNotifier(true); - - ref.listen(tabBarDismissableControllerProvider, (previous, next) { - displayAppBar.value = !next; - }); - ref.listen(overlayControllerProvider, (previous, next) { if (next != null) { overlayController.show(); @@ -330,14 +310,19 @@ class BrowserScreen extends HookConsumerWidget { final sheetDisplayed = displayedSheet != null; // Compute visibility states for toolbars - final appBarVisible = useValueListenable(displayAppBar); + final selectedTabId = ref.watch(selectedTabProvider); + final toolbarState = ref.watch( + toolbarVisibilityControllerProvider(selectedTabId), + ); - // Toolbar is visible when: sheet is shown OR (not fullscreen AND app bar visible) - // appBarVisible reflects both scroll-based auto-hide and swipe dismiss state + // Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible) + // The controller handles loading-start show internally via ref.listen on isLoading + final effectiveAppBarVisible = + toolbarState == ToolbarVisibility.visible; final topToolbarVisible = - sheetDisplayed || (!tabInFullScreen && appBarVisible); + sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible); final bottomToolbarVisible = - sheetDisplayed || (!tabInFullScreen && appBarVisible); + sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible); // Calculate relative safe area for sheet max size final relativeSafeArea = MediaQuery.of(context).relativeSafeArea(); @@ -382,7 +367,6 @@ class BrowserScreen extends HookConsumerWidget { final pixelRatio = MediaQuery.of(context).devicePixelRatio; // Watch find-in-page visibility for the selected tab - final selectedTabId = ref.watch(selectedTabProvider); final findInPageVisible = selectedTabId != null && ref.watch( @@ -446,7 +430,8 @@ class BrowserScreen extends HookConsumerWidget { // Keep clipping state in sync with keyboard and auto-hide states. // While keyboard is visible, do not apply toolbar clipping. useEffect(() { - final targetClippingPx = autoHideTabBar && !keyboardVisible + final targetClippingPx = + autoHideTabBar && !keyboardVisible && !tabIsLoading ? desiredToolbarClippingPx.value : 0; @@ -456,7 +441,7 @@ class BrowserScreen extends HookConsumerWidget { } return null; - }, [autoHideTabBar, keyboardVisible]); + }, [autoHideTabBar, keyboardVisible, tabIsLoading]); final animationProgressCallback = useCallback(( double progress, @@ -467,13 +452,15 @@ class BrowserScreen extends HookConsumerWidget { final clippingPx = -((1.0 - progress) * heightPx * pixelRatio).round(); desiredToolbarClippingPx.value = clippingPx; - final targetClippingPx = keyboardVisible ? 0 : clippingPx; + final targetClippingPx = (keyboardVisible || tabIsLoading) + ? 0 + : clippingPx; if (targetClippingPx != lastClippingPx.value) { lastClippingPx.value = targetClippingPx; unawaited(viewportService.setVerticalClipping(targetClippingPx)); } - }, [keyboardVisible, pixelRatio]); + }, [keyboardVisible, pixelRatio, tabIsLoading]); // Theme with dynamic snackbar margin to position above bottom toolbar final themeData = Theme.of(context).copyWith( @@ -522,7 +509,6 @@ class BrowserScreen extends HookConsumerWidget { bottom: browserBottomOffset, child: _Browser( overlayController: overlayController, - displayAppBar: displayAppBar, tabInFullScreen: tabInFullScreen, pointerMoveEventSink: pointerMoveEventsController.sink, sheetDisplayed: sheetDisplayed, @@ -558,7 +544,6 @@ class BrowserScreen extends HookConsumerWidget { : null, child: _TabBar( tabBarPosition: TabBarPosition.bottom, - displayAppBar: displayAppBar, showMainToolbar: tabBarPosition == TabBarPosition.bottom, showContextualToolbar: showContextualToolbar, showQuickTabSwitcherBar: showQuickTabSwitcherBar, @@ -583,7 +568,6 @@ class BrowserScreen extends HookConsumerWidget { child: _TabBar( tabBarPosition: TabBarPosition.top, showMainToolbar: true, - displayAppBar: displayAppBar, showContextualToolbar: showContextualToolbar, showQuickTabSwitcherBar: showQuickTabSwitcherBar, pointerMoveEvents: pointerMoveEventsController.stream, @@ -729,7 +713,6 @@ class _Browser extends HookConsumerWidget { Duration get _backButtonPressTimeout => const Duration(seconds: 2); final OverlayPortalController overlayController; - final ValueNotifier displayAppBar; final StreamSink pointerMoveEventSink; final bool tabInFullScreen; final bool sheetDisplayed; @@ -737,7 +720,6 @@ class _Browser extends HookConsumerWidget { const _Browser({ required this.overlayController, - required this.displayAppBar, required this.tabInFullScreen, required this.pointerMoveEventSink, required this.sheetDisplayed, @@ -822,9 +804,10 @@ class _Browser extends HookConsumerWidget { } //Make sure app bar is visible - if (!ref.read(tabBarDismissableControllerProvider)) { - displayAppBar.value = true; - } + final tabId = ref.read(selectedTabProvider); + ref + .read(toolbarVisibilityControllerProvider(tabId).notifier) + .show(); if (tabState?.isLoading == true) { lastBackButtonPress.value = null; diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index b6efaac4..f04682af 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -32,12 +32,13 @@ import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; +import 'package:weblibre/features/geckoview/domain/providers.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart'; @@ -59,6 +60,7 @@ import 'package:weblibre/presentation/icons/weblibre_icons.dart'; import 'package:weblibre/presentation/widgets/safe_raw_image.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; +import 'package:weblibre/utils/ui_helper.dart'; class BrowserTopAppBar extends HookConsumerWidget { final bool showMainToolbar; @@ -68,7 +70,8 @@ class BrowserTopAppBar extends HookConsumerWidget { late final BrowserTabBar _tabBar; late final _size = Size.fromHeight(_tabBar.getToolbarHeight()); - BrowserTopAppBar({super.key, + BrowserTopAppBar({ + super.key, required this.showMainToolbar, required this.showContextualToolbar, required this.showQuickTabSwitcherBar, @@ -103,7 +106,8 @@ class BrowserBottomAppBar extends HookConsumerWidget { late final BrowserTabBar _tabBar; late final _size = Size.fromHeight(_tabBar.getToolbarHeight()); - BrowserBottomAppBar({super.key, + BrowserBottomAppBar({ + super.key, required this.showMainToolbar, required this.displayedSheet, required this.showContextualToolbar, @@ -148,7 +152,8 @@ class BrowserTabBar extends HookConsumerWidget { final bool showMainToolbarNavigationButton; final bool showMainToolbarTabActionButton; - const BrowserTabBar({super.key, + const BrowserTabBar({ + super.key, required this.showMainToolbar, required this.displayedSheet, required this.showContextualToolbar, @@ -270,8 +275,23 @@ class BrowserTabBar extends HookConsumerWidget { !distance.dy.isNegative && distance.dy.abs() > dismissThreshold, }; if (shouldDismiss && ref.read(bottomSheetControllerProvider) == null) { - unawaited(HapticFeedback.lightImpact()); - ref.read(tabBarDismissableControllerProvider.notifier).dismiss(); + final viewportService = ref.read(viewportServiceProvider); + if (viewportService.isBrowserHandlingScrollEnabled) { + unawaited(HapticFeedback.lightImpact()); + ref + .read( + toolbarVisibilityControllerProvider(selectedTabId).notifier, + ) + .dismiss(); + } else if (context.mounted) { + showDismissOverrideMessage(context, () { + ref + .read( + toolbarVisibilityControllerProvider(selectedTabId).notifier, + ) + .dismiss(); + }); + } } }, child: Column( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart index 90c0c552..fdd8c067 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart @@ -21,8 +21,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart'; +import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart'; import 'package:weblibre/features/geckoview/features/readerview/domain/providers/readerable.dart'; class BrowserFab extends HookConsumerWidget { @@ -30,7 +31,10 @@ class BrowserFab extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tabBarDismissed = ref.watch(tabBarDismissableControllerProvider); + final selectedTabId = ref.watch(selectedTabProvider); + final toolbarState = ref.watch( + toolbarVisibilityControllerProvider(selectedTabId), + ); final appearanceButtonVisible = ref.watch( appearanceButtonVisibilityProvider.select( @@ -54,12 +58,16 @@ class BrowserFab extends HookConsumerWidget { }, child: const Icon(MdiIcons.formatFont), ); - } else if (tabBarDismissed) { + } else if (toolbarState == ToolbarVisibility.dismissed) { child = FloatingActionButton( key: const ValueKey('dock_fab'), heroTag: 'dock_fab', onPressed: () { - ref.read(tabBarDismissableControllerProvider.notifier).show(); + ref + .read( + toolbarVisibilityControllerProvider(selectedTabId).notifier, + ) + .forceShow(); }, child: const Icon(MdiIcons.dockBottom), ); diff --git a/app/lib/features/user/domain/repositories/profile.g.dart b/app/lib/features/user/domain/repositories/profile.g.dart index 6c8c1ce0..250c14a5 100644 --- a/app/lib/features/user/domain/repositories/profile.g.dart +++ b/app/lib/features/user/domain/repositories/profile.g.dart @@ -33,7 +33,7 @@ final class ProfileRepositoryProvider ProfileRepository create() => ProfileRepository(); } -String _$profileRepositoryHash() => r'c17702af1e59727ab3fec26e9ca659048e92c8bb'; +String _$profileRepositoryHash() => r'b770e7406e1602f808cc8076c1eda67b4fce6b2d'; abstract class _$ProfileRepository extends $AsyncNotifier> { FutureOr> build(); diff --git a/app/lib/utils/ui_helper.dart b/app/lib/utils/ui_helper.dart index 7ec6664e..b900da5a 100644 --- a/app/lib/utils/ui_helper.dart +++ b/app/lib/utils/ui_helper.dart @@ -192,3 +192,19 @@ void showTabUndoClose( ScaffoldMessenger.of(context).showSnackBar(snackBar); } + +void showDismissOverrideMessage( + BuildContext context, + VoidCallback onDismiss, { + Duration duration = const Duration(seconds: 4), +}) { + ScaffoldMessenger.of(context).clearSnackBars(); + + final snackBar = _createFloatingSnackBar( + content: const Text('Hiding disabled by site'), + action: SnackBarAction(label: 'Dismiss', onPressed: onDismiss), + duration: duration, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt index e7f1afe6..c7dc3209 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt @@ -26,6 +26,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.BrowserHandlingScrollFeature import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature @@ -97,6 +98,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit // Keyboard visibility detection feature private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null + // Browser scroll-handling detection feature + private var browserHandlingScrollFeature: BrowserHandlingScrollFeature? = null + // Registers a photo picker activity launcher in single-select mode. private val singleMediaPicker = AndroidPhotoPicker.singleMediaPicker( @@ -500,6 +504,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit keyboardVisibilityFeature = KeyboardVisibilityFeature(viewportEvents).also { it.start(binding.root) } + + browserHandlingScrollFeature = BrowserHandlingScrollFeature(viewportEvents).also { + it.start() + } } onEngineSetupComplete() @@ -614,6 +622,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit keyboardVisibilityFeature?.stop() keyboardVisibilityFeature = null + // Stop browser scroll-handling detection + browserHandlingScrollFeature?.stop() + browserHandlingScrollFeature = null + GlobalComponents.onPullToRefreshEnabledChanged = null val engineView = fragmentEngineView engineView?.setActivityContext(null) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt index 1f768395..852e2d5b 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt @@ -216,7 +216,8 @@ class GeckoTabsApiImpl : GeckoTabsApi { progress = tab.content.progress.toLong(), isPrivate = tab.content.private, isFullScreen = tab.content.fullScreen, - isLoading = tab.content.loading + isLoading = tab.content.loading, + showToolbarAsExpanded = tab.content.showToolbarAsExpanded, ) ) { } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Events.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Events.kt index dc6b710a..f7f94d2e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Events.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Events.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState import mozilla.components.feature.addons.logger @@ -188,7 +189,8 @@ class Events( it.content.private, it.content.fullScreen, it.content.progress, - it.content.loading + it.content.loading, + it.content.showToolbarAsExpanded, ) } .debounce(15) @@ -204,9 +206,19 @@ class Events( progress = tab.content.progress.toLong(), isPrivate = tab.content.private, isFullScreen = tab.content.fullScreen, - isLoading = tab.content.loading + isLoading = tab.content.loading, + showToolbarAsExpanded = tab.content.showToolbarAsExpanded, ) ) { _ -> } + + // Reset showToolbarAsExpanded after forwarding to Flutter, + // mirroring Fenix ToolbarBehaviorController behavior. + // This ensures subsequent expand events trigger a new state change. + if (tab.content.showToolbarAsExpanded) { + stateFlow.dispatch( + ContentAction.UpdateExpandedToolbarStateAction(tab.id, false) + ) + } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/BrowserHandlingScrollFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/BrowserHandlingScrollFeature.kt new file mode 100644 index 00000000..bea5d173 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/BrowserHandlingScrollFeature.kt @@ -0,0 +1,124 @@ +/* + * 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.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import eu.weblibre.flutter_mozilla_components.GlobalComponents +import eu.weblibre.flutter_mozilla_components.ext.EventSequence +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents +import org.mozilla.gecko.util.ThreadUtils.runOnUiThread + +/** + * Feature that reports whether GeckoView is currently handling scroll input. + * + * Checks InputResultDetail while active and only emits on state changes. + */ +class BrowserHandlingScrollFeature( + private val flutterEvents: GeckoViewportEvents, +) { + private var running = false + private var touchSessionActive = false + private var lastValue: Boolean? = null + private var touchListener: View.OnTouchListener? = null + private var touchTargetView: View? = null + + fun start() { + if (running) return + running = true + touchSessionActive = false + lastValue = null + attachTouchListenerIfPossible() + } + + fun stop() { + if (!running) return + running = false + touchSessionActive = false + detachTouchListenerIfPossible() + lastValue = null + } + + private fun attachTouchListenerIfPossible() { + val engineViewRoot = GlobalComponents.components?.mainBrowserEngineView?.asView() + if (engineViewRoot == null) return + + if (touchListener != null) return + + val targetView = resolveTouchTargetView(engineViewRoot) + + touchListener = View.OnTouchListener { _, event -> + if (!running) return@OnTouchListener false + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + touchSessionActive = true + emitIfChanged() + } + + MotionEvent.ACTION_MOVE -> { + if (touchSessionActive) { + emitIfChanged() + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + touchSessionActive = false + } + } + + // Never consume touch; GeckoView must keep handling it. + false + } + + targetView.setOnTouchListener(touchListener) + touchTargetView = targetView + } + + private fun detachTouchListenerIfPossible() { + if (touchTargetView != null && touchListener != null) { + touchTargetView?.setOnTouchListener(null) + } + touchListener = null + touchTargetView = null + } + + private fun resolveTouchTargetView(root: View): View { + if (root !is ViewGroup) return root + + // GeckoEngineView wraps the actual NestedGeckoView as its first child. + if (root.childCount > 0) { + return root.getChildAt(0) + } + + return root + } + + private fun emitIfChanged() { + val isHandling = try { + val engineView = GlobalComponents.components?.mainBrowserEngineView + if (engineView == null) { + false + } else { + val detail = engineView.getInputResultDetail() + detail.canScrollToTop() || detail.canScrollToBottom() + } + } catch (_: Throwable) { + false + } + + if (lastValue == isHandling) return + lastValue = isHandling + + val sequence = EventSequence.next() + runOnUiThread { + flutterEvents.onBrowserHandlingScrollChanged(sequence, isHandling) { } + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 3fa46e53..536e5caa 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -1597,7 +1597,8 @@ data class TabContentState ( val progress: Long, val isPrivate: Boolean, val isFullScreen: Boolean, - val isLoading: Boolean + val isLoading: Boolean, + val showToolbarAsExpanded: Boolean ) { companion object { @@ -1611,7 +1612,8 @@ data class TabContentState ( val isPrivate = pigeonVar_list[6] as Boolean val isFullScreen = pigeonVar_list[7] as Boolean val isLoading = pigeonVar_list[8] as Boolean - return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading) + val showToolbarAsExpanded = pigeonVar_list[9] as Boolean + return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading, showToolbarAsExpanded) } } fun toList(): List { @@ -1625,6 +1627,7 @@ data class TabContentState ( isPrivate, isFullScreen, isLoading, + showToolbarAsExpanded, ) } override fun equals(other: Any?): Boolean { @@ -6919,6 +6922,31 @@ class GeckoViewportEvents(private val binaryMessenger: BinaryMessenger, private } } } + /** + * Called when GeckoView scroll-handling eligibility changes. + * + * [sequence] Event sequence number for ordering. + * [isHandling] True when browser content can consume scrolling for + * dynamic toolbar behavior. False when content is not scrollable or + * the page consumed touch input. + */ + fun onBrowserHandlingScrollChanged(sequenceArg: Long, isHandlingArg: Boolean, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(sequenceArg, isHandlingArg)) { + 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 { diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart index 1f0fdd39..4937c806 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart @@ -30,6 +30,7 @@ class GeckoViewportService extends GeckoViewportEvents { isVisible: false, isAnimating: false, )); + final _browserHandlingScrollSubject = BehaviorSubject.seeded(false); /// Stream of keyboard visibility changes. /// @@ -46,6 +47,17 @@ class GeckoViewportService extends GeckoViewportEvents { /// Whether the keyboard is currently visible. bool get isKeyboardVisible => _keyboardSubject.value.isVisible; + /// Stream of browser scroll-handling eligibility. + /// + /// Emits true when GeckoView reports that browser content can handle + /// scrolling for dynamic toolbar behavior. + ValueStream get browserHandlingScrollEvents => + _browserHandlingScrollSubject.stream; + + /// Current browser scroll-handling eligibility. + bool get isBrowserHandlingScrollEnabled => + _browserHandlingScrollSubject.value; + /// Creates a new viewport service. /// /// Call [setUp] to register the event handlers after construction. @@ -113,8 +125,14 @@ class GeckoViewportService extends GeckoViewportEvents { )); } + @override + void onBrowserHandlingScrollChanged(int sequence, bool isHandling) { + _browserHandlingScrollSubject.addWhenMoreRecent(sequence, null, isHandling); + } + /// Disposes the service and closes all streams. Future dispose() async { await _keyboardSubject.close(); + await _browserHandlingScrollSubject.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index e48c3568..c4dc1540 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -1735,6 +1735,7 @@ class TabContentState { required this.isPrivate, required this.isFullScreen, required this.isLoading, + required this.showToolbarAsExpanded, }); String id; @@ -1755,6 +1756,8 @@ class TabContentState { bool isLoading; + bool showToolbarAsExpanded; + List _toList() { return [ id, @@ -1766,6 +1769,7 @@ class TabContentState { isPrivate, isFullScreen, isLoading, + showToolbarAsExpanded, ]; } @@ -1784,6 +1788,7 @@ class TabContentState { isPrivate: result[6]! as bool, isFullScreen: result[7]! as bool, isLoading: result[8]! as bool, + showToolbarAsExpanded: result[9]! as bool, ); } @@ -8133,6 +8138,14 @@ abstract class GeckoViewportEvents { /// [isAnimating] Whether the keyboard is currently animating. void onKeyboardVisibilityChanged(int sequence, int heightPx, bool isVisible, bool isAnimating); + /// Called when GeckoView scroll-handling eligibility changes. + /// + /// [sequence] Event sequence number for ordering. + /// [isHandling] True when browser content can consume scrolling for + /// dynamic toolbar behavior. False when content is not scrollable or + /// the page consumed touch input. + void onBrowserHandlingScrollChanged(int sequence, bool isHandling); + static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { @@ -8169,6 +8182,34 @@ abstract class GeckoViewportEvents { }); } } + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$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.onBrowserHandlingScrollChanged was null.'); + final List args = (message as List?)!; + final int? arg_sequence = (args[0] as int?); + assert(arg_sequence != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null int.'); + final bool? arg_isHandling = (args[1] as bool?); + assert(arg_isHandling != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null bool.'); + try { + api.onBrowserHandlingScrollChanged(arg_sequence!, arg_isHandling!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 05688a34..fe185f54 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -517,6 +517,7 @@ class TabContentState { final bool isPrivate; final bool isFullScreen; final bool isLoading; + final bool showToolbarAsExpanded; TabContentState( this.id, @@ -528,6 +529,7 @@ class TabContentState { this.isPrivate, this.isFullScreen, this.isLoading, + this.showToolbarAsExpanded, ); } @@ -1746,6 +1748,14 @@ abstract class GeckoViewportEvents { bool isVisible, bool isAnimating, ); + + /// Called when GeckoView scroll-handling eligibility changes. + /// + /// [sequence] Event sequence number for ordering. + /// [isHandling] True when browser content can consume scrolling for + /// dynamic toolbar behavior. False when content is not scrollable or + /// the page consumed touch input. + void onBrowserHandlingScrollChanged(int sequence, bool isHandling); } // =============================================================================