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 displayedSheet = ref.watch(bottomSheetControllerProvider);
final autoHideTabBar = switch (tabBarPosition) {
TabBarPosition.top => false,
TabBarPosition.bottom => ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.autoHideTabBar,
),
};
);
// Auto-hide scroll detection hooks (run unconditionally per hook rules)
final diffAcc = useRef(0.0);
@@ -154,7 +151,7 @@ class _TabBar extends HookConsumerWidget {
}, [tabId, autoHideTabBar]);
useOnAppLifecycleStateChange((previous, current) {
if (!autoHideTabBar) return;
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (current == AppLifecycleState.resumed) {
resetHiddenState();
}
@@ -164,7 +161,7 @@ class _TabBar extends HookConsumerWidget {
previous,
next,
) {
if (!autoHideTabBar) return;
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next == true) {
resetHiddenState();
}
@@ -174,7 +171,7 @@ class _TabBar extends HookConsumerWidget {
previous,
next,
) {
if (!autoHideTabBar) return;
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
if (next != null && previous != null) {
if (previous != next) {
resetHiddenState();
@@ -185,7 +182,7 @@ class _TabBar extends HookConsumerWidget {
useOnStreamChange(
pointerMoveEvents,
onData: (event) {
if (!autoHideTabBar) return;
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
final diff = event.dy;
if (diff < 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);
ref.listen(tabBarDismissableControllerProvider, (previous, next) {
@@ -286,11 +289,11 @@ class BrowserScreen extends HookConsumerWidget {
// Compute visibility states for toolbars
final appBarVisible = useValueListenable(displayAppBar);
final topDismissed = ref.watch(tabBarDismissableControllerProvider);
// 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 =
sheetDisplayed || (!tabInFullScreen && !topDismissed);
sheetDisplayed || (!tabInFullScreen && appBarVisible);
final bottomToolbarVisible =
sheetDisplayed || (!tabInFullScreen && appBarVisible);
@@ -310,6 +313,29 @@ class BrowserScreen extends HookConsumerWidget {
final bottomAppBarTotalHeight =
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
final themeData = Theme.of(context).copyWith(
bottomSheetTheme: BottomSheetThemeData(
@@ -343,14 +369,21 @@ class BrowserScreen extends HookConsumerWidget {
resizeToAvoidBottomInset: false,
body: Stack(
children: [
// Layer 0: Browser content (fills entire Stack - constant dimensions)
Positioned.fill(
// Layer 0: Browser content
// 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(
overlayController: overlayController,
displayAppBar: displayAppBar,
tabInFullScreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventsController.sink,
sheetDisplayed: sheetDisplayed,
hasTopBarOffset: browserTopOffset > 0,
),
),
@@ -382,7 +415,10 @@ class BrowserScreen extends HookConsumerWidget {
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
showContextualToolbar: showContextualToolbar,
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,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
pointerMoveEvents: null,
pointerMoveEvents: pointerMoveEventsController.stream,
),
),
),
@@ -493,6 +529,7 @@ class _Browser extends HookConsumerWidget {
final StreamSink<Offset> pointerMoveEventSink;
final bool tabInFullScreen;
final bool sheetDisplayed;
final bool hasTopBarOffset;
const _Browser({
required this.overlayController,
@@ -500,6 +537,7 @@ class _Browser extends HookConsumerWidget {
required this.tabInFullScreen,
required this.pointerMoveEventSink,
required this.sheetDisplayed,
required this.hasTopBarOffset,
});
@override
@@ -654,6 +692,7 @@ class _Browser extends HookConsumerWidget {
child: _BrowserView(
isFullscreen: tabInFullScreen,
pointerMoveEventSink: pointerMoveEventSink,
hasTopBarOffset: hasTopBarOffset,
),
),
),
@@ -666,13 +705,19 @@ class _Browser extends HookConsumerWidget {
class _BrowserView extends StatelessWidget {
final bool isFullscreen;
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
Widget build(BuildContext context) {
return SafeArea(
top: !isFullscreen,
// Disable top SafeArea when top bar handles it (has offset applied)
top: !isFullscreen && !hasTopBarOffset,
right: !isFullscreen,
// Bottom SafeArea is handled by the overlay toolbar (BottomAppBar)
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 toolbarHeight = useMemoized(() => getToolbarHeight());
@@ -259,9 +265,18 @@ class BrowserTabBar extends HookConsumerWidget {
}
}
}
} else if (distance.dy < (toolbarHeight / 3) &&
distance.dx.abs() < 15) {
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
} else if (distance.dx.abs() < 15) {
// Swipe direction for dismiss depends on toolbar position:
// - 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(