1import { HeaderHeightContext } from "@react-navigation/elements";
2import { use } from "react";
3import { Platform } from "react-native";
4
5import { useAdaptiveWorkspaceLayout } from "../../features/layout/adaptive-workspace-context";
6import { NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET } from "../../features/layout/native-mail-search-toolbar";
7import { useNativeMailSearchToolbar } from "../../native/use-native-mail-search-toolbar";
8import type { AppBarSearch } from "./AppBar.types";
9import { useSafeAreaInsets } from "react-native-safe-area-context";
10
11import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
12import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
13import { useToolbarLayout } from "./useToolbarLayout";
14
15/**
16 * How far below the screen's top edge its AppBar ends: where content that scrolls under a
17 * translucent bar starts, and where an overlay pinned under the bar goes. Measured by the native
18 * column or stack on iOS; Android draws the bar in-tree from the toolbar geometry.
19 */
20export function useBarInset(): number {
21 const headerHeight = use(HeaderHeightContext);
22 const columnMetrics = useNativeColumnLayoutMetrics();
23 const insets = useSafeAreaInsets();
24 const toolbar = useToolbarLayout();
25 if (Platform.OS === "android") {
26 return toolbar.paddingTop + toolbar.height + toolbar.paddingBottom;
27 }
28 return headerHeight ?? columnMetrics?.safeArea.top ?? insets.top + IOS_NAV_BAR_HEIGHT;
29}
30
31/**
32 * How far the bar covers the top of the body below it: the bar inset where the body runs under
33 * the iOS bar, 0 on Android, where the bar sits above the body in flow.
34 */
35export function useBarOverlap(): number {
36 const barInset = useBarInset();
37 return Platform.OS === "android" ? 0 : barInset;
38}
39
40/**
41 * The content's top safe area under the AppBar: the height of the iOS bar its column lays content
42 * under, glass or solid, and zero on Android, where the bar sits in flow. For manual scroll
43 * content insets.
44 */
45export function useContentTopInset(): number {
46 const insets = useSafeAreaInsets();
47 const columnMetrics = useNativeColumnLayoutMetrics();
48 return Platform.OS === "ios" ? (columnMetrics?.safeArea.top ?? insets.top) : 0;
49}
50
51/** The content's bottom safe area on iOS, which a list with manual insets must add itself. */
52export function useContentBottomInset(): number {
53 const insets = useSafeAreaInsets();
54 const columnMetrics = useNativeColumnLayoutMetrics();
55 return Platform.OS === "ios" ? (columnMetrics?.safeArea.bottom ?? insets.bottom) : 0;
56}
57
58/** How much of the screen's bottom edge an AppBar `search` covers: iOS's bottom search toolbar. */
59export function useSearchToolbarInset(search: AppBarSearch | undefined): number {
60 const { layout } = useAdaptiveWorkspaceLayout();
61 const usesNativeMailSearchToolbar = useNativeMailSearchToolbar();
62 return search !== undefined &&
63 (search.compactToolbar ?? !layout.usesSplitView) &&
64 usesNativeMailSearchToolbar
65 ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET
66 : 0;
67}
68
69/** Where a sheet's scrolling content ends: clear of the bottom safe area, with a little room. */
70export function useSheetContentBottomInset(): number {
71 const insets = useSafeAreaInsets();
72 const columnMetrics = useNativeColumnLayoutMetrics();
73 return Platform.OS === "ios"
74 ? (columnMetrics?.safeArea.bottom ?? insets.bottom) + 8
75 : Math.max(insets.bottom, 18) + 18;
76}
77
78/** The bottom safe area a list in a sheet pads itself: Android's, which its sheets don't adjust for. */
79export function useUnadjustedBottomInset(): number {
80 const insets = useSafeAreaInsets();
81 return Platform.OS === "android" ? insets.bottom : 0;
82}