Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 79 additions & 25 deletions apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,19 +366,32 @@ function AdaptiveWorkspaceLayoutContent(
const width = viewportWidth - navigationRailWidth;
// The sidebar stays mounted under a workspace page, so its list is ready on the way back.
const shouldRenderPrimarySidebar = layout.usesSplitView;
const inspectorSizing = Platform.OS === "android" ? "android" : undefined;
const auxiliaryPanePreferredWidth = fileInspectorPreferredWidth ?? undefined;
const inspectorWindow = useMemo(
() =>
Platform.OS === "android"
? { width: viewportWidth, leadingWidth: viewportWidth - width, hinge: windowHinge }
: undefined,
[viewportWidth, width, windowHinge],
);
const fileInspector = useMemo(
() =>
deriveFileInspectorPaneLayout({
layout,
viewportWidth: width,
preferredWidth: fileInspectorPreferredWidth ?? undefined,
inspectorSizing,
inspectorWindow,
preferredWidth: auxiliaryPanePreferredWidth,
reservedLeadingWidth:
shouldRenderPrimarySidebar && showPrimarySidebar && !sidebarFloats
? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0)
: 0,
}),
[
fileInspectorPreferredWidth,
auxiliaryPanePreferredWidth,
inspectorSizing,
inspectorWindow,
layout,
showPrimarySidebar,
shouldRenderPrimarySidebar,
Expand Down Expand Up @@ -406,21 +419,25 @@ function AdaptiveWorkspaceLayoutContent(
deriveWorkspacePaneLayout({
layout,
viewportWidth: width,
inspectorSizing,
inspectorWindow,
horizontalSafeArea,
nativeWorkspace,
primarySidebarFloats: sidebarFloats,
primarySidebarPreferredVisible: showPrimarySidebar,
// Only a column a screen fills makes room, so a stale preference can't hide the sidebar.
auxiliaryPanePreferredVisible:
!workspacePage && inspectorActive && fileInspectorPreferredVisible,
auxiliaryPanePreferredWidth: fileInspectorPreferredWidth ?? undefined,
auxiliaryPanePreferredWidth,
nativeInspectorWidth,
}),
[
nativeInspectorWidth,
horizontalSafeArea,
fileInspectorPreferredVisible,
fileInspectorPreferredWidth,
auxiliaryPanePreferredWidth,
inspectorSizing,
inspectorWindow,
inspectorActive,
layout,
nativeWorkspace,
Expand Down Expand Up @@ -454,6 +471,7 @@ function AdaptiveWorkspaceLayoutContent(
),
);
setFileInspectorPreferredVisible(true);
if (Platform.OS === "android" && sidebarFloats) setPrimarySidebarVisible(false);
}

return () => {
Expand All @@ -478,15 +496,22 @@ function AdaptiveWorkspaceLayoutContent(
if (primarySidebarPinned) {
return;
}
if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) {
if (
!panes.primarySidebarVisible &&
(panes.primarySidebarSuppressedByAuxiliary ||
(Platform.OS === "android" && !sidebarFloats && panes.auxiliaryPaneVisible))
) {
setFileInspectorPreferredVisible(false);
setPrimarySidebarPreferredVisible(true);
setPrimarySidebarVisible(true);
return;
}
setPrimarySidebarVisible(!preferredSidebarVisible);
};
const revealPrimarySidebar = () => {
if (panes.primarySidebarSuppressedByAuxiliary) {
if (
panes.primarySidebarSuppressedByAuxiliary ||
(Platform.OS === "android" && !sidebarFloats && panes.auxiliaryPaneVisible)
) {
setFileInspectorPreferredVisible(false);
}
setPrimarySidebarVisible(true);
Expand All @@ -504,6 +529,7 @@ function AdaptiveWorkspaceLayoutContent(
};
const showInspector = () => {
setFileInspectorPreferredVisible(true);
if (Platform.OS === "android" && sidebarFloats) setPrimarySidebarVisible(false);
if (fileInspector.supported) {
setInspectorSheetRoute(null);
return;
Expand All @@ -526,8 +552,8 @@ function AdaptiveWorkspaceLayoutContent(
};
useHardwareKeyboardCommand("files", handleOpenFilesCommand);
const toggleAuxiliaryPane = () => {
if (fileInspector.supported) {
setFileInspectorPreferredVisible((current) => !current);
if (fileInspector.supported && fileInspectorPreferredVisible) {
setFileInspectorPreferredVisible(false);
setInspectorSheetRoute(null);
} else if (inspectorSheetOpen) dismissInspectorSheet();
else showInspector();
Expand Down Expand Up @@ -628,8 +654,10 @@ function AdaptiveWorkspaceLayoutContent(
const inspectorColumnTargetWidth = panes.auxiliaryPaneVisible
? (panes.auxiliaryPaneWidth ?? 0)
: 0;
// Android's split content is a rounded card: keep it off the screen edge like the list card.
const contentEndGap = Platform.OS === "android" && layout.usesSplitView ? 4 : 0;
const inspectorGap = panes.auxiliaryPaneGap ?? 0;
// ScreenContent supplies both margins at an inspector boundary.
const contentEndGap =
Platform.OS === "android" && layout.usesSplitView && !panes.auxiliaryPaneVisible ? 4 : 0;
// Android's split is one strip: a sidebar slot of fixed width, then the detail pane. The
// sidebar never resizes; both slide by one offset, so together they always fill the width.
const stripSidebar =
Expand All @@ -648,6 +676,7 @@ function AdaptiveWorkspaceLayoutContent(
0,
(contentSpansSidebar ? width : panes.contentPaneWidth) -
inspectorColumnTargetWidth -
inspectorGap -
contentEndGap,
);
// A workspace page (Usage) lays out at this width even while hidden behind another tab, so
Expand Down Expand Up @@ -720,12 +749,11 @@ function AdaptiveWorkspaceLayoutContent(
// against the edge that stays put while the other one slides, so nothing reflows after the
// panes land. A sliding sidebar uncovers the strip it is about to fill, or covers the content
// until it is gone.
const contentLeftTarget = sidebarFloats
? navigationRailWidth
: navigationRailWidth +
(shouldRenderPrimarySidebar && !workspacePage && panes.primarySidebarVisible
? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0)
: 0);
const contentLeftTarget =
navigationRailWidth +
(!sidebarFloats && shouldRenderPrimarySidebar && !workspacePage && panes.primarySidebarVisible
? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0)
: 0);
const [contentAnchor, setContentAnchor] = useState({
viewportWidth,
left: contentLeftTarget,
Expand Down Expand Up @@ -805,20 +833,28 @@ function AdaptiveWorkspaceLayoutContent(
scrimOpacity.set(withTiming(floatingSidebarOpen ? 0.32 : 0, WORKSPACE_PANE_TIMING));
}, [floatingSidebarOpen, scrimOpacity]);
const scrimAnimatedStyle = useAnimatedStyle(() => ({ opacity: scrimOpacity.value }));
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Back closes a floating sidebar before it leaves the page
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Back closes Android panes or a floating sidebar before leaving the page
useEffect(() => {
if (!floatingSidebarOpen || nativeWorkspace) return;
const inspectorOpen = Platform.OS === "android" && panes.auxiliaryPaneVisible;
if ((!floatingSidebarOpen && !inspectorOpen) || nativeWorkspace) return;
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
// A sheet or modal over the workspace takes Back first.
const state = rootStackState(navigation);
if (state && workspaceRoutesOf(state.routes.slice(0, state.index + 1)).overlays.length > 0) {
return false;
}
setPrimarySidebarVisible(false);
if (floatingSidebarOpen) setPrimarySidebarVisible(false);
else setFileInspectorPreferredVisible(false);
return true;
});
return () => subscription.remove();
}, [floatingSidebarOpen, nativeWorkspace, navigation, setPrimarySidebarVisible]);
}, [
floatingSidebarOpen,
panes.auxiliaryPaneVisible,
nativeWorkspace,
navigation,
setPrimarySidebarVisible,
]);
// Showing or hiding the sidebar clips a fixed-width list. A hinge resize instead tracks the
// animated edge, so the header actions slide with it rather than jumping or being covered.
const listPaneGap = layout.listPaneGap ?? 0;
Expand Down Expand Up @@ -854,7 +890,8 @@ function AdaptiveWorkspaceLayoutContent(
viewportWidth -
renderedRailWidth.value -
(workspacePage || sidebarFloats ? 0 : renderedSidebarWidth.value) -
renderedInspectorWidth.value,
renderedInspectorWidth.value -
inspectorGap,
),
);
// Android can resize the native row before React removes its split panes on a fold.
Expand All @@ -871,7 +908,7 @@ function AdaptiveWorkspaceLayoutContent(
// back to the row, so a column after it stays put while the strip slides.
width: Math.max(
0,
viewportWidth - renderedRailWidth.value - renderedInspectorWidth.value,
viewportWidth - renderedRailWidth.value - renderedInspectorWidth.value - inspectorGap,
),
marginRight: -sidebarSlotWidth,
transform: [{ translateX: stripOffset.value }],
Expand Down Expand Up @@ -908,6 +945,21 @@ function AdaptiveWorkspaceLayoutContent(
[sidebarInLandscape, sidebarInPortrait, sidebarOverRotates],
);

const setAuxiliaryPaneWidth = (preferredWidth: number) => {
const next = deriveFileInspectorPaneLayout({
layout,
viewportWidth: width,
inspectorSizing,
inspectorWindow,
preferredWidth,
reservedLeadingWidth:
shouldRenderPrimarySidebar && showPrimarySidebar && !sidebarFloats
? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0)
: 0,
});
if (next.width !== null) setFileInspectorPreferredWidth(next.width);
};

const actions = useStableActions({
startNewTask: handleStartNewTask,
selectDetail: handleSelectDetail,
Expand All @@ -919,7 +971,7 @@ function AdaptiveWorkspaceLayoutContent(
revealPrimarySidebar,
setPrimarySidebarVisible,
openWorkspaceTab,
setAuxiliaryPaneWidth: setFileInspectorPreferredWidth,
setAuxiliaryPaneWidth,
setNativeInspectorWidth: (width: number) =>
setNativeInspectorWidthFraction(inspectorWidthFor(width / viewportWidth) / viewportWidth),
});
Expand Down Expand Up @@ -1109,13 +1161,15 @@ function AdaptiveWorkspaceLayoutContent(
/>
</Animated.View>
) : null}
{inspectorGap > 0 ? <View style={{ width: inspectorGap }} /> : null}
<PaneContext value="inspector">
<WorkspaceInspectorPane
renderedInspectorWidth={renderedInspectorWidth}
active={workspaceInspector?.active ?? false}
obscured={floatingSidebarOpen}
panes={panes}
content={workspaceInspector?.content}
setAuxiliaryPaneWidth={setFileInspectorPreferredWidth}
setAuxiliaryPaneWidth={setAuxiliaryPaneWidth}
onClose={() => setFileInspectorPreferredVisible(false)}
onClosed={handleWorkspaceInspectorClosed}
/>
Expand Down
19 changes: 8 additions & 11 deletions apps/mobile/src/features/layout/workspace-inspector-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Animated, {
type SharedValue,
} from "react-native-reanimated";

import { constrainAuxiliaryPaneWidth, type WorkspacePaneLayout } from "../../lib/layout";
import type { WorkspacePaneLayout } from "../../lib/layout";
import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
import { WorkspacePaneDivider } from "./workspace-pane-divider";
import { InspectorPanes } from "./inspector-host";
Expand All @@ -22,6 +22,7 @@ const PANE_EASING = cubicBezier(0.65, 0, 0.35, 1);
export function WorkspaceInspectorPane(props: {
readonly renderedInspectorWidth: SharedValue<number>;
readonly active?: boolean;
readonly obscured?: boolean;
readonly onClose: () => void;
readonly onClosed?: () => void;
readonly panes: WorkspacePaneLayout;
Expand All @@ -33,6 +34,7 @@ export function WorkspaceInspectorPane(props: {
const inspectorSupported = props.content !== undefined && inspectorWidth !== null;
const inspectorVisible =
inspectorSupported && panes.auxiliaryPaneVisible && (props.active ?? true);
const inspectorInteractive = inspectorVisible && !props.obscured;
const resizeStartWidth = useRef(0);
const [resizing, setResizing] = useState(false);

Expand Down Expand Up @@ -84,20 +86,15 @@ export function WorkspaceInspectorPane(props: {
setResizing(true);
};
const resizeBy = (delta: number) => {
setAuxiliaryPaneWidth(
constrainAuxiliaryPaneWidth({
preferredWidth: resizeStartWidth.current + delta,
availableWidth: panes.contentPaneWidth,
}),
);
setAuxiliaryPaneWidth(resizeStartWidth.current + delta);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
};
const endResize = () => {
setResizing(false);
};

return (
<>
{inspectorVisible ? (
{inspectorInteractive && (panes.auxiliaryPaneGap ?? 0) === 0 ? (
<WorkspacePaneDivider
accessibilityLabel="Resize detail pane"
currentWidth={inspectorWidth ?? 0}
Expand All @@ -110,10 +107,10 @@ export function WorkspaceInspectorPane(props: {
{inspectorSupported ? (
<Animated.View
className="shrink-0 overflow-hidden"
accessibilityElementsHidden={!inspectorVisible}
accessibilityElementsHidden={!inspectorInteractive}
collapsable={false}
importantForAccessibility={inspectorVisible ? "auto" : "no-hide-descendants"}
pointerEvents={inspectorVisible ? "auto" : "none"}
importantForAccessibility={inspectorInteractive ? "auto" : "no-hide-descendants"}
pointerEvents={inspectorInteractive ? "auto" : "none"}
style={inspectorStyle}
>
<Animated.View className="flex-1" style={inspectorContentStyle}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { StackActions, useNavigation } from "@react-navigation/native";
import { createContext, use } from "react";
import { Platform } from "react-native";

import { useWorkspacePaneState } from "../layout/AdaptiveWorkspaceLayout";

Expand All @@ -25,7 +24,7 @@ export function useOpenThreadInspector() {
source: { readonly environmentId: string; readonly threadId: string },
target: ThreadInspectorTarget,
) => {
if (Platform.OS !== "ios" || !fileInspector.supported) return false;
if (!fileInspector.supported) return false;
if (openInInspector) openInInspector(target);
else {
const parent = navigation.getState()?.routes.findLast((route) => route.name === "Thread");
Expand Down
Loading
Loading