fix content overlay issues; top bar now also hiding on scroll;

This commit is contained in:
Fabian Freund
2026-01-09 19:45:27 +01:00
parent bf906038ef
commit f009dd4342
2 changed files with 82 additions and 22 deletions
@@ -126,14 +126,11 @@ class _TabBar extends HookConsumerWidget {
final tabId = ref.watch(selectedTabProvider); final tabId = ref.watch(selectedTabProvider);
final displayedSheet = ref.watch(bottomSheetControllerProvider); final displayedSheet = ref.watch(bottomSheetControllerProvider);
final autoHideTabBar = switch (tabBarPosition) { final autoHideTabBar = ref.watch(
TabBarPosition.top => false, generalSettingsWithDefaultsProvider.select(
TabBarPosition.bottom => ref.watch( (value) => value.autoHideTabBar,
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
), ),
}; );
// Auto-hide scroll detection hooks (run unconditionally per hook rules) // Auto-hide scroll detection hooks (run unconditionally per hook rules)
final diffAcc = useRef(0.0); final diffAcc = useRef(0.0);
@@ -154,7 +151,7 @@ class _TabBar extends HookConsumerWidget {
}, [tabId, autoHideTabBar]); }, [tabId, autoHideTabBar]);
useOnAppLifecycleStateChange((previous, current) { useOnAppLifecycleStateChange((previous, current) {
if (!autoHideTabBar) return; if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (current == AppLifecycleState.resumed) { if (current == AppLifecycleState.resumed) {
resetHiddenState(); resetHiddenState();
} }
@@ -164,7 +161,7 @@ class _TabBar extends HookConsumerWidget {
previous, previous,
next, next,
) { ) {
if (!autoHideTabBar) return; if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next == true) { if (next == true) {
resetHiddenState(); resetHiddenState();
} }
@@ -174,7 +171,7 @@ class _TabBar extends HookConsumerWidget {
previous, previous,
next, next,
) { ) {
if (!autoHideTabBar) return; if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next != null && previous != null) { if (next != null && previous != null) {
if (previous != next) { if (previous != next) {
resetHiddenState(); resetHiddenState();
@@ -185,7 +182,7 @@ class _TabBar extends HookConsumerWidget {
useOnStreamChange( useOnStreamChange(
pointerMoveEvents, pointerMoveEvents,
onData: (event) { onData: (event) {
if (!autoHideTabBar) return; if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
final diff = event.dy; final diff = event.dy;
if (diff < 0) { if (diff < 0) {
if (diffAcc.value > 0) { if (diffAcc.value > 0) {
@@ -255,6 +252,12 @@ class BrowserScreen extends HookConsumerWidget {
), ),
); );
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
);
final displayAppBar = useValueNotifier(true); final displayAppBar = useValueNotifier(true);
ref.listen(tabBarDismissableControllerProvider, (previous, next) { ref.listen(tabBarDismissableControllerProvider, (previous, next) {
@@ -286,11 +289,11 @@ class BrowserScreen extends HookConsumerWidget {
// Compute visibility states for toolbars // Compute visibility states for toolbars
final appBarVisible = useValueListenable(displayAppBar); final appBarVisible = useValueListenable(displayAppBar);
final topDismissed = ref.watch(tabBarDismissableControllerProvider);
// Toolbar is visible when: sheet is shown OR (not fullscreen AND app bar visible) // 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
final topToolbarVisible = final topToolbarVisible =
sheetDisplayed || (!tabInFullScreen && !topDismissed); sheetDisplayed || (!tabInFullScreen && appBarVisible);
final bottomToolbarVisible = final bottomToolbarVisible =
sheetDisplayed || (!tabInFullScreen && appBarVisible); sheetDisplayed || (!tabInFullScreen && appBarVisible);
@@ -310,6 +313,29 @@ class BrowserScreen extends HookConsumerWidget {
final bottomAppBarTotalHeight = final bottomAppBarTotalHeight =
bottomAppBarContentSize.height + bottomSafeArea; bottomAppBarContentSize.height + bottomSafeArea;
// When auto-hide is disabled, constrain browser to not extend behind toolbar
// (unless toolbar is manually dismissed via swipe gesture)
final browserBottomOffset = (!autoHideTabBar && bottomToolbarVisible)
? bottomAppBarTotalHeight
: 0.0;
// Calculate top toolbar size for browser offset
final topSafeArea = MediaQuery.of(context).padding.top;
final topAppBarContentSize = BrowserTopAppBar(
showMainToolbar: tabBarPosition == TabBarPosition.top,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
).preferredSize;
final topAppBarTotalHeight = topAppBarContentSize.height + topSafeArea;
// For top bar: always constrain browser below toolbar when visible
// (unlike bottom bar which only constrains when auto-hide is disabled)
// This ensures top-of-page content is always accessible
final browserTopOffset =
(tabBarPosition == TabBarPosition.top && topToolbarVisible)
? topAppBarTotalHeight
: 0.0;
// Theme with dynamic snackbar margin to position above bottom toolbar // Theme with dynamic snackbar margin to position above bottom toolbar
final themeData = Theme.of(context).copyWith( final themeData = Theme.of(context).copyWith(
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
@@ -343,14 +369,21 @@ class BrowserScreen extends HookConsumerWidget {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
children: [ children: [
// Layer 0: Browser content (fills entire Stack - constant dimensions) // Layer 0: Browser content
Positioned.fill( // Position changes instantly (no animation) to avoid jarring native view resize
// The toolbar itself animates, providing visual continuity
Positioned(
left: 0,
right: 0,
top: browserTopOffset,
bottom: browserBottomOffset,
child: _Browser( child: _Browser(
overlayController: overlayController, overlayController: overlayController,
displayAppBar: displayAppBar, displayAppBar: displayAppBar,
tabInFullScreen: tabInFullScreen, tabInFullScreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventsController.sink, pointerMoveEventSink: pointerMoveEventsController.sink,
sheetDisplayed: sheetDisplayed, sheetDisplayed: sheetDisplayed,
hasTopBarOffset: browserTopOffset > 0,
), ),
), ),
@@ -382,7 +415,10 @@ class BrowserScreen extends HookConsumerWidget {
showMainToolbar: tabBarPosition == TabBarPosition.bottom, showMainToolbar: tabBarPosition == TabBarPosition.bottom,
showContextualToolbar: showContextualToolbar, showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar, showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: pointerMoveEventsController.stream, // Only subscribe to scroll events when this is the active position
pointerMoveEvents: tabBarPosition == TabBarPosition.bottom
? pointerMoveEventsController.stream
: null,
), ),
), ),
), ),
@@ -402,7 +438,7 @@ class BrowserScreen extends HookConsumerWidget {
displayAppBar: displayAppBar, displayAppBar: displayAppBar,
showContextualToolbar: showContextualToolbar, showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar, showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: null, pointerMoveEvents: pointerMoveEventsController.stream,
), ),
), ),
), ),
@@ -493,6 +529,7 @@ class _Browser extends HookConsumerWidget {
final StreamSink<Offset> pointerMoveEventSink; final StreamSink<Offset> pointerMoveEventSink;
final bool tabInFullScreen; final bool tabInFullScreen;
final bool sheetDisplayed; final bool sheetDisplayed;
final bool hasTopBarOffset;
const _Browser({ const _Browser({
required this.overlayController, required this.overlayController,
@@ -500,6 +537,7 @@ class _Browser extends HookConsumerWidget {
required this.tabInFullScreen, required this.tabInFullScreen,
required this.pointerMoveEventSink, required this.pointerMoveEventSink,
required this.sheetDisplayed, required this.sheetDisplayed,
required this.hasTopBarOffset,
}); });
@override @override
@@ -654,6 +692,7 @@ class _Browser extends HookConsumerWidget {
child: _BrowserView( child: _BrowserView(
isFullscreen: tabInFullScreen, isFullscreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventSink, pointerMoveEventSink: pointerMoveEventSink,
hasTopBarOffset: hasTopBarOffset,
), ),
), ),
), ),
@@ -666,13 +705,19 @@ class _Browser extends HookConsumerWidget {
class _BrowserView extends StatelessWidget { class _BrowserView extends StatelessWidget {
final bool isFullscreen; final bool isFullscreen;
final StreamSink<Offset>? pointerMoveEventSink; final StreamSink<Offset>? pointerMoveEventSink;
final bool hasTopBarOffset;
const _BrowserView({required this.isFullscreen, this.pointerMoveEventSink}); const _BrowserView({
required this.isFullscreen,
required this.hasTopBarOffset,
this.pointerMoveEventSink,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
top: !isFullscreen, // Disable top SafeArea when top bar handles it (has offset applied)
top: !isFullscreen && !hasTopBarOffset,
right: !isFullscreen, right: !isFullscreen,
// Bottom SafeArea is handled by the overlay toolbar (BottomAppBar) // Bottom SafeArea is handled by the overlay toolbar (BottomAppBar)
bottom: false, bottom: false,
@@ -210,6 +210,12 @@ class BrowserTabBar extends HookConsumerWidget {
), ),
); );
final tabBarPosition = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarPosition,
),
);
final dragStartPosition = useRef(Offset.zero); final dragStartPosition = useRef(Offset.zero);
final toolbarHeight = useMemoized(() => getToolbarHeight()); final toolbarHeight = useMemoized(() => getToolbarHeight());
@@ -259,9 +265,18 @@ class BrowserTabBar extends HookConsumerWidget {
} }
} }
} }
} else if (distance.dy < (toolbarHeight / 3) && } else if (distance.dx.abs() < 15) {
distance.dx.abs() < 15) { // Swipe direction for dismiss depends on toolbar position:
ref.read(tabBarDismissableControllerProvider.notifier).dismiss(); // - Bottom bar: swipe down to dismiss (distance.dy negative or small positive)
// - Top bar: swipe up to dismiss (distance.dy positive or small negative)
final dismissThreshold = toolbarHeight / 3;
final shouldDismiss = switch (tabBarPosition) {
TabBarPosition.bottom => distance.dy < dismissThreshold,
TabBarPosition.top => distance.dy > -dismissThreshold,
};
if (shouldDismiss) {
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
}
} }
}, },
child: Column( child: Column(