improve toolbar logic
This commit is contained in:
+3
-3
@@ -75,6 +75,8 @@ class ToolbarVisibilityController extends _$ToolbarVisibilityController {
|
|||||||
|
|
||||||
/// Hide toolbar via scroll. All guards checked internally.
|
/// Hide toolbar via scroll. All guards checked internally.
|
||||||
void requestHide() {
|
void requestHide() {
|
||||||
|
if (state != ToolbarVisibility.visible) return;
|
||||||
|
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||||
if (!settings.autoHideTabBar) return;
|
if (!settings.autoHideTabBar) return;
|
||||||
|
|
||||||
@@ -84,15 +86,13 @@ class ToolbarVisibilityController extends _$ToolbarVisibilityController {
|
|||||||
final viewportService = ref.read(viewportServiceProvider);
|
final viewportService = ref.read(viewportServiceProvider);
|
||||||
if (!viewportService.isBrowserHandlingScrollEnabled) return;
|
if (!viewportService.isBrowserHandlingScrollEnabled) return;
|
||||||
|
|
||||||
if (state == ToolbarVisibility.dismissed) return;
|
|
||||||
|
|
||||||
state = ToolbarVisibility.hidden;
|
state = ToolbarVisibility.hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show toolbar (scroll-up, tab change, loading start, etc).
|
/// Show toolbar (scroll-up, tab change, loading start, etc).
|
||||||
/// Won't show if manually dismissed — use forceShow() for that.
|
/// Won't show if manually dismissed — use forceShow() for that.
|
||||||
void show() {
|
void show() {
|
||||||
if (state == ToolbarVisibility.dismissed) return;
|
if (state != ToolbarVisibility.hidden) return;
|
||||||
state = ToolbarVisibility.visible;
|
state = ToolbarVisibility.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,21 +61,10 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
|
|||||||
import 'package:weblibre/utils/move_to_background.dart';
|
import 'package:weblibre/utils/move_to_background.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
/// Callback for toolbar animation progress updates.
|
|
||||||
/// [progress] is 0.0 when hidden, 1.0 when fully visible.
|
|
||||||
/// [heightPx] is the toolbar height in pixels.
|
|
||||||
typedef ToolbarAnimationCallback =
|
|
||||||
void Function(double progress, double heightPx);
|
|
||||||
|
|
||||||
/// Animated toolbar that slides in/out without changing layout constraints.
|
|
||||||
/// Uses SlideTransition to animate visual transform while maintaining
|
|
||||||
/// constant intrinsic size for layout purposes.
|
|
||||||
class _AnimatedToolbar extends HookWidget {
|
class _AnimatedToolbar extends HookWidget {
|
||||||
final bool visible;
|
final bool visible;
|
||||||
final TabBarPosition position;
|
final TabBarPosition position;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final double toolbarHeight;
|
|
||||||
final ToolbarAnimationCallback? onAnimationProgress;
|
|
||||||
|
|
||||||
static const _kAnimationDuration = Duration(milliseconds: 250);
|
static const _kAnimationDuration = Duration(milliseconds: 250);
|
||||||
|
|
||||||
@@ -83,9 +72,6 @@ class _AnimatedToolbar extends HookWidget {
|
|||||||
required this.visible,
|
required this.visible,
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.toolbarHeight,
|
|
||||||
// ignore: unused_element_parameter
|
|
||||||
this.onAnimationProgress,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -95,19 +81,6 @@ class _AnimatedToolbar extends HookWidget {
|
|||||||
initialValue: visible ? 1.0 : 0.0,
|
initialValue: visible ? 1.0 : 0.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Listen to animation changes and report progress
|
|
||||||
useEffect(() {
|
|
||||||
if (onAnimationProgress == null) return null;
|
|
||||||
|
|
||||||
void listener() {
|
|
||||||
onAnimationProgress?.call(controller.value, toolbarHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.addListener(listener);
|
|
||||||
listener();
|
|
||||||
return () => controller.removeListener(listener);
|
|
||||||
}, [onAnimationProgress, toolbarHeight]);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
unawaited(controller.forward());
|
unawaited(controller.forward());
|
||||||
@@ -352,21 +325,10 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
||||||
final sheetDisplayed = displayedSheet != null;
|
final sheetDisplayed = displayedSheet != null;
|
||||||
|
|
||||||
// Compute visibility states for toolbars
|
// Track selected tab. Toolbar visibility is watched in scoped Consumer
|
||||||
|
// widgets below to avoid rebuilding the entire BrowserScreen on every
|
||||||
|
// hide/show cycle during scrolling.
|
||||||
final selectedTabId = ref.watch(selectedTabProvider);
|
final selectedTabId = ref.watch(selectedTabProvider);
|
||||||
final toolbarState = ref.watch(
|
|
||||||
toolbarVisibilityControllerProvider(selectedTabId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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 toolbarManuallyDismissed =
|
|
||||||
toolbarState == ToolbarVisibility.dismissed;
|
|
||||||
final topToolbarVisible =
|
|
||||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
|
||||||
final bottomToolbarVisible =
|
|
||||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
|
||||||
|
|
||||||
// Calculate relative safe area for sheet max size
|
// Calculate relative safe area for sheet max size
|
||||||
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
|
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
|
||||||
@@ -384,13 +346,7 @@ 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
|
// Calculate top toolbar size for browser offset and progress indicator
|
||||||
// (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 topSafeArea = MediaQuery.of(context).padding.top;
|
||||||
final topAppBarContentSize = BrowserTopAppBar(
|
final topAppBarContentSize = BrowserTopAppBar(
|
||||||
showMainToolbar: tabBarPosition == TabBarPosition.top,
|
showMainToolbar: tabBarPosition == TabBarPosition.top,
|
||||||
@@ -399,14 +355,6 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
).preferredSize;
|
).preferredSize;
|
||||||
final topAppBarTotalHeight = topAppBarContentSize.height + topSafeArea;
|
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;
|
|
||||||
|
|
||||||
// Get pixel ratio for converting logical pixels to physical pixels
|
// Get pixel ratio for converting logical pixels to physical pixels
|
||||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
@@ -425,11 +373,30 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
: 0.0;
|
: 0.0;
|
||||||
final findInPageHeightPx = (findInPageHeight * pixelRatio).round();
|
final findInPageHeightPx = (findInPageHeight * pixelRatio).round();
|
||||||
|
|
||||||
final stableToolbarHeight = autoHideTabBar && !toolbarManuallyDismissed
|
// Track dismissed state without triggering full rebuild on every
|
||||||
|
// hide/show. Updated via ref.listen below and synced on tab switch.
|
||||||
|
final toolbarDismissed = useState(false);
|
||||||
|
|
||||||
|
// Sync dismissed state when selected tab changes
|
||||||
|
useEffect(() {
|
||||||
|
toolbarDismissed.value =
|
||||||
|
ref.read(toolbarVisibilityControllerProvider(selectedTabId)) ==
|
||||||
|
ToolbarVisibility.dismissed;
|
||||||
|
return null;
|
||||||
|
}, [selectedTabId]);
|
||||||
|
|
||||||
|
final stableToolbarHeight =
|
||||||
|
autoHideTabBar && !toolbarDismissed.value && !tabInFullScreen
|
||||||
? bottomAppBarTotalHeight
|
? bottomAppBarTotalHeight
|
||||||
: 0.0;
|
: 0.0;
|
||||||
final stableToolbarHeightPx = (stableToolbarHeight * pixelRatio).round();
|
final stableToolbarHeightPx = (stableToolbarHeight * pixelRatio).round();
|
||||||
|
|
||||||
|
// Compute toolbar visibility for keyboard inset math.
|
||||||
|
// Uses ref.watch via the toolbarDismissed useState to avoid
|
||||||
|
// subscribing to every hide/show toggle.
|
||||||
|
final toolbarVisibleForLayout =
|
||||||
|
sheetDisplayed || (!tabInFullScreen && !toolbarDismissed.value);
|
||||||
|
|
||||||
final keyboardHeightPx = useState<int?>(null);
|
final keyboardHeightPx = useState<int?>(null);
|
||||||
|
|
||||||
useOnStreamChange(
|
useOnStreamChange(
|
||||||
@@ -446,7 +413,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final keyboardVisible = keyboardHeightPx.value != null;
|
final keyboardVisible = keyboardHeightPx.value != null;
|
||||||
|
|
||||||
final bottomLayoutReservedPx = (!autoHideTabBar && bottomToolbarVisible)
|
final bottomLayoutReservedPx = (!autoHideTabBar && toolbarVisibleForLayout)
|
||||||
? (bottomAppBarTotalHeight * pixelRatio).round()
|
? (bottomAppBarTotalHeight * pixelRatio).round()
|
||||||
: 0;
|
: 0;
|
||||||
final keyboardViewportHeightPx = keyboardVisible
|
final keyboardViewportHeightPx = keyboardVisible
|
||||||
@@ -469,63 +436,70 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [effectiveToolbarHeightPx]);
|
}, [effectiveToolbarHeightPx]);
|
||||||
|
|
||||||
// Track last clipping value to avoid redundant platform channel calls
|
// Track last clipping value to avoid redundant platform channel calls.
|
||||||
|
// Clipping is set once when visibility changes (before animation starts),
|
||||||
|
// rather than on every animation frame, to avoid platform channel spam.
|
||||||
final lastClippingPx = useRef(0);
|
final lastClippingPx = useRef(0);
|
||||||
final desiredToolbarClippingPx = useRef(0);
|
|
||||||
|
|
||||||
// Keep clipping state in sync with keyboard and auto-hide states.
|
void updateClipping() {
|
||||||
// While keyboard is visible, do not apply toolbar clipping.
|
final toolbarState = ref.read(
|
||||||
useEffect(
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
() {
|
);
|
||||||
if (toolbarManuallyDismissed) {
|
final effectiveVisible = toolbarState == ToolbarVisibility.visible;
|
||||||
desiredToolbarClippingPx.value = 0;
|
final dismissed = toolbarState == ToolbarVisibility.dismissed;
|
||||||
}
|
|
||||||
|
|
||||||
final targetClippingPx =
|
// When hidden: clip from bottom by toolbar height so GeckoView
|
||||||
autoHideTabBar &&
|
// can compute its layout while Flutter animates.
|
||||||
!toolbarManuallyDismissed &&
|
// When visible/dismissed or overridden by keyboard/loading: no clipping.
|
||||||
!keyboardVisible &&
|
final targetClippingPx =
|
||||||
!tabIsLoading
|
autoHideTabBar &&
|
||||||
? desiredToolbarClippingPx.value
|
!dismissed &&
|
||||||
: 0;
|
!effectiveVisible &&
|
||||||
|
!keyboardVisible &&
|
||||||
if (targetClippingPx != lastClippingPx.value) {
|
!tabIsLoading &&
|
||||||
lastClippingPx.value = targetClippingPx;
|
!tabInFullScreen
|
||||||
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
? -(bottomAppBarTotalHeight * pixelRatio).round()
|
||||||
}
|
: 0;
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[autoHideTabBar, toolbarManuallyDismissed, keyboardVisible, tabIsLoading],
|
|
||||||
);
|
|
||||||
|
|
||||||
final animationProgressCallback = useCallback((
|
|
||||||
double progress,
|
|
||||||
double heightPx,
|
|
||||||
) {
|
|
||||||
if (toolbarManuallyDismissed) {
|
|
||||||
desiredToolbarClippingPx.value = 0;
|
|
||||||
if (lastClippingPx.value != 0) {
|
|
||||||
lastClippingPx.value = 0;
|
|
||||||
unawaited(viewportService.setVerticalClipping(0));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip content from bottom as toolbar hides.
|
|
||||||
// Negative clipping = clip from bottom.
|
|
||||||
final clippingPx = -((1.0 - progress) * heightPx * pixelRatio).round();
|
|
||||||
desiredToolbarClippingPx.value = clippingPx;
|
|
||||||
|
|
||||||
final targetClippingPx = (keyboardVisible || tabIsLoading)
|
|
||||||
? 0
|
|
||||||
: clippingPx;
|
|
||||||
|
|
||||||
if (targetClippingPx != lastClippingPx.value) {
|
if (targetClippingPx != lastClippingPx.value) {
|
||||||
lastClippingPx.value = targetClippingPx;
|
lastClippingPx.value = targetClippingPx;
|
||||||
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
||||||
}
|
}
|
||||||
}, [toolbarManuallyDismissed, keyboardVisible, pixelRatio, tabIsLoading]);
|
}
|
||||||
|
|
||||||
|
// Update clipping when toolbar visibility changes (does NOT trigger
|
||||||
|
// full rebuild). Also tracks dismissed state via useState so that
|
||||||
|
// stableToolbarHeight / effectiveToolbarHeightPx stay correct.
|
||||||
|
ref.listen(toolbarVisibilityControllerProvider(selectedTabId), (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
updateClipping();
|
||||||
|
|
||||||
|
// Update dismissed state — triggers a targeted rebuild only when
|
||||||
|
// the dismissed flag actually changes (not on every hide/show).
|
||||||
|
final isDismissed = next == ToolbarVisibility.dismissed;
|
||||||
|
if (toolbarDismissed.value != isDismissed) {
|
||||||
|
toolbarDismissed.value = isDismissed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update clipping when other relevant state changes
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
updateClipping();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
autoHideTabBar,
|
||||||
|
keyboardVisible,
|
||||||
|
tabIsLoading,
|
||||||
|
tabInFullScreen,
|
||||||
|
bottomAppBarTotalHeight,
|
||||||
|
pixelRatio,
|
||||||
|
selectedTabId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -545,7 +519,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom:
|
bottom:
|
||||||
(bottomToolbarVisible ? bottomAppBarContentSize.height + 8 : 16) +
|
bottomAppBarContentSize.height +
|
||||||
|
8 +
|
||||||
(findInPageVisible ? findInPageHeight : 0),
|
(findInPageVisible ? findInPageHeight : 0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -565,18 +540,45 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
// Layer 0: Browser content
|
// Layer 0: Browser content
|
||||||
// Position changes instantly (no animation) to avoid jarring native view resize
|
// Position changes instantly (no animation) to avoid jarring native view resize
|
||||||
// The toolbar itself animates, providing visual continuity
|
// The toolbar itself animates, providing visual continuity
|
||||||
Positioned(
|
Consumer(
|
||||||
left: 0,
|
builder: (context, ref, child) {
|
||||||
right: 0,
|
final toolbarState = ref.watch(
|
||||||
top: browserTopOffset,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
bottom: browserBottomOffset,
|
);
|
||||||
child: _Browser(
|
final toolbarVisible =
|
||||||
overlayController: overlayController,
|
sheetDisplayed ||
|
||||||
tabInFullScreen: tabInFullScreen,
|
(!tabInFullScreen &&
|
||||||
pointerMoveEventSink: pointerMoveEventsController.sink,
|
toolbarState == ToolbarVisibility.visible);
|
||||||
sheetDisplayed: sheetDisplayed,
|
|
||||||
hasTopBarOffset: browserTopOffset > 0,
|
// When auto-hide is disabled, constrain browser above toolbar
|
||||||
),
|
// (unless toolbar is manually dismissed via swipe gesture)
|
||||||
|
final bottomOffset = (!autoHideTabBar && toolbarVisible)
|
||||||
|
? bottomAppBarTotalHeight
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
// For top bar: constrain browser below toolbar when visible
|
||||||
|
// to ensure top-of-page content is always accessible
|
||||||
|
final topOffset =
|
||||||
|
(tabBarPosition == TabBarPosition.top && toolbarVisible)
|
||||||
|
? topAppBarTotalHeight
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: topOffset,
|
||||||
|
bottom: bottomOffset,
|
||||||
|
child: _Browser(
|
||||||
|
overlayController: overlayController,
|
||||||
|
tabInFullScreen: tabInFullScreen,
|
||||||
|
pointerMoveEventSink: autoHideTabBar
|
||||||
|
? pointerMoveEventsController.sink
|
||||||
|
: null,
|
||||||
|
sheetDisplayed: sheetDisplayed,
|
||||||
|
hasTopBarOffset: topOffset > 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 1: Sheet (when displayed) - positioned above toolbar
|
// Layer 1: Sheet (when displayed) - positioned above toolbar
|
||||||
@@ -598,24 +600,31 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: _AnimatedToolbar(
|
child: Consumer(
|
||||||
position: TabBarPosition.bottom,
|
builder: (context, ref, _) {
|
||||||
visible: bottomToolbarVisible,
|
final toolbarState = ref.watch(
|
||||||
toolbarHeight: bottomAppBarTotalHeight,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
onAnimationProgress:
|
);
|
||||||
autoHideTabBar && !toolbarManuallyDismissed
|
final visible =
|
||||||
? animationProgressCallback
|
sheetDisplayed ||
|
||||||
: null,
|
(!tabInFullScreen &&
|
||||||
child: _TabBar(
|
toolbarState == ToolbarVisibility.visible);
|
||||||
tabBarPosition: TabBarPosition.bottom,
|
return _AnimatedToolbar(
|
||||||
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
|
position: TabBarPosition.bottom,
|
||||||
showContextualToolbar: showContextualToolbar,
|
visible: visible,
|
||||||
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
child: _TabBar(
|
||||||
// Only subscribe to scroll events when this is the active position
|
tabBarPosition: TabBarPosition.bottom,
|
||||||
pointerMoveEvents: tabBarPosition == TabBarPosition.bottom
|
showMainToolbar:
|
||||||
? pointerMoveEventsController.stream
|
tabBarPosition == TabBarPosition.bottom,
|
||||||
: null,
|
showContextualToolbar: showContextualToolbar,
|
||||||
),
|
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
||||||
|
pointerMoveEvents:
|
||||||
|
tabBarPosition == TabBarPosition.bottom
|
||||||
|
? pointerMoveEventsController.stream
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -625,44 +634,76 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: _AnimatedToolbar(
|
child: Consumer(
|
||||||
position: TabBarPosition.top,
|
builder: (context, ref, _) {
|
||||||
visible: topToolbarVisible,
|
final toolbarState = ref.watch(
|
||||||
toolbarHeight: topAppBarTotalHeight,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
child: _TabBar(
|
);
|
||||||
tabBarPosition: TabBarPosition.top,
|
final visible =
|
||||||
showMainToolbar: true,
|
sheetDisplayed ||
|
||||||
showContextualToolbar: showContextualToolbar,
|
(!tabInFullScreen &&
|
||||||
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
toolbarState == ToolbarVisibility.visible);
|
||||||
pointerMoveEvents: pointerMoveEventsController.stream,
|
return _AnimatedToolbar(
|
||||||
),
|
position: TabBarPosition.top,
|
||||||
|
visible: visible,
|
||||||
|
child: _TabBar(
|
||||||
|
tabBarPosition: TabBarPosition.top,
|
||||||
|
showMainToolbar: true,
|
||||||
|
showContextualToolbar: showContextualToolbar,
|
||||||
|
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
||||||
|
pointerMoveEvents: pointerMoveEventsController.stream,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 4: FAB (draggable via long press)
|
// Layer 4: FAB (draggable via long press)
|
||||||
DraggableFab(
|
Consumer(
|
||||||
bottomToolbarVisible: bottomToolbarVisible,
|
builder: (context, ref, child) {
|
||||||
bottomAppBarHeight: bottomAppBarTotalHeight,
|
final toolbarState = ref.watch(
|
||||||
bottomSafeArea: bottomSafeArea,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
|
);
|
||||||
|
final visible =
|
||||||
|
sheetDisplayed ||
|
||||||
|
(!tabInFullScreen &&
|
||||||
|
toolbarState == ToolbarVisibility.visible);
|
||||||
|
return DraggableFab(
|
||||||
|
bottomToolbarVisible: visible,
|
||||||
|
bottomAppBarHeight: bottomAppBarTotalHeight,
|
||||||
|
bottomSafeArea: bottomSafeArea,
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: const BrowserFab(),
|
child: const BrowserFab(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 5: Page load progress indicator (animates with toolbar visibility)
|
// Layer 5: Page load progress indicator (animates with toolbar visibility)
|
||||||
AnimatedPositioned(
|
Consumer(
|
||||||
duration: _AnimatedToolbar._kAnimationDuration,
|
builder: (context, ref, child) {
|
||||||
curve: Curves.easeInOutQuart,
|
final toolbarState = ref.watch(
|
||||||
left: 0,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
right: 0,
|
);
|
||||||
top: tabBarPosition == TabBarPosition.top && topToolbarVisible
|
final visible =
|
||||||
? topAppBarTotalHeight
|
sheetDisplayed ||
|
||||||
: null,
|
(!tabInFullScreen &&
|
||||||
bottom:
|
toolbarState == ToolbarVisibility.visible);
|
||||||
tabBarPosition == TabBarPosition.bottom &&
|
return AnimatedPositioned(
|
||||||
bottomToolbarVisible
|
duration: _AnimatedToolbar._kAnimationDuration,
|
||||||
? bottomAppBarTotalHeight
|
curve: Curves.easeInOutQuart,
|
||||||
: tabBarPosition == TabBarPosition.bottom
|
left: 0,
|
||||||
? 0
|
right: 0,
|
||||||
: null,
|
top: tabBarPosition == TabBarPosition.top && visible
|
||||||
|
? topAppBarTotalHeight
|
||||||
|
: null,
|
||||||
|
bottom: tabBarPosition == TabBarPosition.bottom && visible
|
||||||
|
? bottomAppBarTotalHeight
|
||||||
|
: tabBarPosition == TabBarPosition.bottom
|
||||||
|
? 0
|
||||||
|
: null,
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final value = ref.watch(
|
final value = ref.watch(
|
||||||
@@ -685,17 +726,27 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
|
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
|
||||||
AnimatedPositioned(
|
Consumer(
|
||||||
duration: _AnimatedToolbar._kAnimationDuration,
|
builder: (context, ref, child) {
|
||||||
curve: Curves.easeInOutQuart,
|
final toolbarState = ref.watch(
|
||||||
left: 0,
|
toolbarVisibilityControllerProvider(selectedTabId),
|
||||||
right: 0,
|
);
|
||||||
bottom: math.max(
|
final visible =
|
||||||
bottomToolbarVisible
|
sheetDisplayed ||
|
||||||
? bottomAppBarTotalHeight
|
(!tabInFullScreen &&
|
||||||
: bottomSafeArea,
|
toolbarState == ToolbarVisibility.visible);
|
||||||
MediaQuery.viewInsetsOf(context).bottom,
|
return AnimatedPositioned(
|
||||||
),
|
duration: _AnimatedToolbar._kAnimationDuration,
|
||||||
|
curve: Curves.easeInOutQuart,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: math.max(
|
||||||
|
visible ? bottomAppBarTotalHeight : bottomSafeArea,
|
||||||
|
MediaQuery.viewInsetsOf(context).bottom,
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final tabId = ref.watch(selectedTabProvider);
|
final tabId = ref.watch(selectedTabProvider);
|
||||||
@@ -777,7 +828,7 @@ class _Browser extends HookConsumerWidget {
|
|||||||
Duration get _backButtonPressTimeout => const Duration(seconds: 2);
|
Duration get _backButtonPressTimeout => const Duration(seconds: 2);
|
||||||
|
|
||||||
final OverlayPortalController overlayController;
|
final OverlayPortalController overlayController;
|
||||||
final StreamSink<Offset> pointerMoveEventSink;
|
final StreamSink<Offset>? pointerMoveEventSink;
|
||||||
final bool tabInFullScreen;
|
final bool tabInFullScreen;
|
||||||
final bool sheetDisplayed;
|
final bool sheetDisplayed;
|
||||||
final bool hasTopBarOffset;
|
final bool hasTopBarOffset;
|
||||||
|
|||||||
+20
-1
@@ -90,6 +90,10 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
DateTime? _suggestionCountTime;
|
DateTime? _suggestionCountTime;
|
||||||
|
|
||||||
|
static const _pointerThrottleInterval = Duration(milliseconds: 32);
|
||||||
|
DateTime _lastPointerEvent = DateTime(0);
|
||||||
|
Offset _accumulatedDelta = Offset.zero;
|
||||||
|
|
||||||
Future<void> _timerTick(Timer timer) async {
|
Future<void> _timerTick(Timer timer) async {
|
||||||
await ref
|
await ref
|
||||||
.read(selectedTabSessionProvider)
|
.read(selectedTabSessionProvider)
|
||||||
@@ -280,10 +284,25 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
return Listener(
|
return Listener(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPointerUp: (widget.pointerMoveEventSink != null)
|
||||||
|
? (_) {
|
||||||
|
if (_accumulatedDelta != Offset.zero) {
|
||||||
|
widget.pointerMoveEventSink!.add(_accumulatedDelta);
|
||||||
|
_accumulatedDelta = Offset.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
onPointerMove: (widget.pointerMoveEventSink != null)
|
onPointerMove: (widget.pointerMoveEventSink != null)
|
||||||
? (event) {
|
? (event) {
|
||||||
if (event.down) {
|
if (event.down) {
|
||||||
widget.pointerMoveEventSink!.add(event.localDelta);
|
_accumulatedDelta += event.localDelta;
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (now.difference(_lastPointerEvent) >=
|
||||||
|
_pointerThrottleInterval) {
|
||||||
|
_lastPointerEvent = now;
|
||||||
|
widget.pointerMoveEventSink!.add(_accumulatedDelta);
|
||||||
|
_accumulatedDelta = Offset.zero;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
-12
@@ -8,7 +8,6 @@ package eu.weblibre.flutter_mozilla_components.api
|
|||||||
|
|
||||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of GeckoViewportApi that controls GeckoView's viewport behavior
|
* Implementation of GeckoViewportApi that controls GeckoView's viewport behavior
|
||||||
@@ -22,12 +21,6 @@ import mozilla.components.support.base.log.logger.Logger
|
|||||||
* is called, the value is stored and applied when the EngineView becomes available.
|
* is called, the value is stored and applied when the EngineView becomes available.
|
||||||
*/
|
*/
|
||||||
class GeckoViewportApiImpl : GeckoViewportApi {
|
class GeckoViewportApiImpl : GeckoViewportApi {
|
||||||
companion object {
|
|
||||||
private const val TAG = "GeckoViewportApi"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logger = Logger(TAG)
|
|
||||||
|
|
||||||
private val components by lazy {
|
private val components by lazy {
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
}
|
}
|
||||||
@@ -46,13 +39,11 @@ class GeckoViewportApiImpl : GeckoViewportApi {
|
|||||||
|
|
||||||
val engineView = components.mainBrowserEngineView
|
val engineView = components.mainBrowserEngineView
|
||||||
if (engineView == null) {
|
if (engineView == null) {
|
||||||
logger.debug("$TAG: setDynamicToolbarMaxHeight($height) - mainBrowserEngineView not ready, storing as pending")
|
|
||||||
pendingToolbarHeight = height
|
pendingToolbarHeight = height
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingToolbarHeight = null
|
pendingToolbarHeight = null
|
||||||
logger.debug("$TAG: setDynamicToolbarMaxHeight($height)")
|
|
||||||
engineView.setDynamicToolbarMaxHeight(height)
|
engineView.setDynamicToolbarMaxHeight(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +55,6 @@ class GeckoViewportApiImpl : GeckoViewportApi {
|
|||||||
val pending = pendingToolbarHeight ?: return
|
val pending = pendingToolbarHeight ?: return
|
||||||
val engineView = components.mainBrowserEngineView ?: return
|
val engineView = components.mainBrowserEngineView ?: return
|
||||||
pendingToolbarHeight = null
|
pendingToolbarHeight = null
|
||||||
logger.debug("$TAG: Applying pending toolbar height: $pending")
|
|
||||||
engineView.setDynamicToolbarMaxHeight(pending)
|
engineView.setDynamicToolbarMaxHeight(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +68,9 @@ class GeckoViewportApiImpl : GeckoViewportApi {
|
|||||||
|
|
||||||
val engineView = components.mainBrowserEngineView
|
val engineView = components.mainBrowserEngineView
|
||||||
if (engineView == null) {
|
if (engineView == null) {
|
||||||
logger.warn("$TAG: setVerticalClipping called but mainBrowserEngineView is null")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("$TAG: setVerticalClipping($clipping)")
|
|
||||||
engineView.setVerticalClipping(clipping)
|
engineView.setVerticalClipping(clipping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user