Skip to content
Source
Sign inSign up

AndroidWorkspaceNavigation.android.tsx

195 lines7.3 KB

1import { selectedNavigationTab, visibleNavigationTabs } from "./navigation-tab-order";
2import { requireNativeView } from "expo";
3import { CommonActions, useNavigation } from "@react-navigation/native";
4import {
5 Keyboard,
6 processColor,
7 useWindowDimensions,
8 View,
9 type NativeSyntheticEvent,
10 type ViewProps,
11} from "react-native";
12import { useSafeAreaInsets } from "react-native-safe-area-context";
13​
14import { useAndroidControlSizing } from "../../../components/ui/useAndroidControlSizing";
15import { useToolbarLayout } from "../../../components/ui/useToolbarLayout";
16import { rootNavigationRef } from "../../../lib/rootNavigation";
17import { useWorkspaceConnectionState } from "../../../state/workspace";
18import { androidWorkspaceNavigationColors } from "./android-workspace-navigation-colors";
19import {
20 androidWorkspaceTabNavigationState,
21 type AndroidWorkspaceNavigationTab,
22} from "./android-workspace-tabs";
23import { useAppearancePreferences } from "../../settings/appearance/AppearancePreferencesProvider";
24import { useSoftKeyboardShowing } from "../../keyboard/useSoftKeyboardShowing";
25import { useAdaptiveWorkspaceLayout } from "../adaptive-workspace-context";
26import { useAndroidWorkspaceRoot } from "./AndroidWorkspaceTabsProvider";
27import { useNavigationTabOrder, useHiddenNavigationTabs } from "./navigation-tabs";
28import { useWorkspaceTabs } from "./WorkspaceTabsContext";
29import { FLOATING_TABS, floatingTabsMetrics } from "./floating-workspace-tabs";
30​
31// Matches RAIL_WIDTH in the native rail.
32export const ANDROID_WORKSPACE_RAIL_WIDTH = 72;
33​
34interface NativeNavigationProps extends ViewProps {
35 readonly rail: boolean;
36 readonly floating: boolean;
37 readonly tabs: readonly string[];
38 readonly selectedTab: AndroidWorkspaceNavigationTab;
39 readonly dark: boolean;
40 readonly textScale: number;
41 readonly newEnabled: boolean;
42 readonly safeAreaTop: number;
43 readonly safeAreaBottom: number;
44 readonly headerHeight: number;
45 readonly containerColor: number;
46 readonly selectedColor: number;
47 readonly indicatorColor: number;
48 readonly unselectedColor: number;
49 readonly actionColor: number;
50 readonly actionContentColor: number;
51 readonly onTabSelected: (
52 event: NativeSyntheticEvent<{ readonly tab: AndroidWorkspaceNavigationTab }>,
53 ) => void;
54 readonly onNewThread: () => void;
55 readonly onOverlayChange: (event: NativeSyntheticEvent<{ readonly overlay: boolean }>) => void;
57​
58const NativeNavigation = requireNativeView<NativeNavigationProps>("T3WorkspaceNavigation");
59​
60function nativeColor(value: string) {
61 const color = processColor(value);
62 return typeof color === "number" ? color : 0;
64​
65export function AndroidWorkspaceNavigation(props: {
66 readonly onNewThread: () => void;
67 readonly rail?: boolean;
68 readonly floating?: boolean;
69 readonly fillWidth?: boolean;
70 readonly paneWidth?: number;
71 readonly onSelect?: () => void;
72 /** The rail needs the whole workspace while it widens over the list. */
73 readonly onOverlayChange?: (overlay: boolean) => void;
74}) {
75 const workspace = useWorkspaceTabs()!;
76 const order = useNavigationTabOrder();
77 const hidden = useHiddenNavigationTabs();
78 const root = useAndroidWorkspaceRoot();
79 const navigation = useNavigation();
80 const insets = useSafeAreaInsets();
81 const toolbar = useToolbarLayout();
82 const connectionState = useWorkspaceConnectionState();
83 const keyboardVisible = useSoftKeyboardShowing();
84 const { layout } = useAdaptiveWorkspaceLayout();
85 const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
86 const { scale, fabSize } = useAndroidControlSizing();
87 const window = useWindowDimensions();
88 const rail = Boolean(props.rail);
89 const floating = !rail && Boolean(props.floating);
90 const tabs = [...visibleNavigationTabs(order, hidden, rail ? 5 : 4), "settings"];
91 const { containerColor, indicatorColor, selectedColor, unselectedColor } =
92 androidWorkspaceNavigationColors(colors, rail, floating);
93 const selectedTab = selectedNavigationTab(
94 order,
95 workspace.state.selectedTab,
96 root?.settingsOpen ?? false,
97 hidden,
98 rail ? 5 : 4,
99 );
100 // FloatingWorkspaceTabs starts the bar at the left inset, so a cutout narrows the room.
101 const barWidth =
102 (props.paneWidth ?? window.width) -
103 (Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.edge);
104 const metrics = floatingTabsMetrics(tabs.length, scale, fabSize, barWidth, props.fillWidth);
105 if (!rail && keyboardVisible) return null;
106​
107 return (
108 <View
109 style={
110 // The rail draws its own surface and pads the system bars; while open it spans the
111 // workspace, so the rest of it must stay transparent.
112 rail
113 ? { flex: 1 }
114 : floating
115 ? undefined
116 : { backgroundColor: containerColor, paddingBottom: insets.bottom }
117 }
118 >
119 <NativeNavigation
120 rail={rail}
121 floating={floating}
122 tabs={tabs}
123 selectedTab={selectedTab}
124 dark={themeAppearance === "dark"}
125 textScale={scale}
126 newEnabled={connectionState.hasConnections}
127 safeAreaTop={toolbar.paddingTop}
128 safeAreaBottom={insets.bottom}
129 headerHeight={toolbar.height}
130 containerColor={nativeColor(containerColor)}
131 selectedColor={nativeColor(selectedColor)}
132 indicatorColor={nativeColor(indicatorColor)}
133 unselectedColor={nativeColor(unselectedColor)}
134 actionColor={nativeColor(colors["--color-primary"])}
135 actionContentColor={nativeColor(colors["--color-primary-foreground"])}
136 onTabSelected={(event) => {
137 Keyboard.dismiss();
138 const tab = event.nativeEvent.tab;
139 if (tab !== "settings") workspace.dispatch({ type: "select", tab });
140 // Reset the root stack even when a nested settings navigator has focus.
141 // The provider renders inside the root stack's layout, so the container's root state is
142 // that stack's current state.
143 const rootState = rootNavigationRef.getRootState();
144 if (root && rootState) {
145 navigation.dispatch({
146 ...CommonActions.reset(
147 androidWorkspaceTabNavigationState(rootState, tab, layout.usesSplitView),
148 ),
149 target: root.key,
150 });
151 }
152 props.onSelect?.();
153 }}
154 onNewThread={() => {
155 Keyboard.dismiss();
156 props.onNewThread();
157 }}
158 onOverlayChange={(event) => props.onOverlayChange?.(event.nativeEvent.overlay)}
159 style={
160 rail
161 ? { flex: 1 }
162 : floating
163 ? {
164 width: metrics.pill + 2 * FLOATING_TABS.shadow,
165 height: metrics.height + 2 * FLOATING_TABS.shadow,
166 }
167 : { height: 80 * Math.max(1, scale) }
168 }
169 />
170 </View>
171 );
173​
174export function FloatingWorkspaceTabs(props: {
175 readonly fillWidth?: boolean;
176 readonly onNewThread: () => void;
177 readonly paneWidth?: number;
178 readonly onSelect?: () => void;
179}) {
180 const insets = useSafeAreaInsets();
181 return (
182 <View
183 pointerEvents="box-none"
184 style={{
185 position: "absolute",
186 left: Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.shadow,
187 right: 0,
188 bottom: insets.bottom + FLOATING_TABS.bottom - FLOATING_TABS.shadow,
189 alignItems: "flex-start",
190 }}
191 >
192 <AndroidWorkspaceNavigation {...props} floating />
193 </View>
194 );