Keep desktop sidebar controls stable across panel changes (#2078)

* fix(app): stabilize desktop sidebar controls

Keep the left-window-controls toggle mounted across sidebar state changes so its position and tooltip state remain stable. Preserve browser placement and show an explicit Explorer close control only where native right-side controls do not provide one.

* fix(app): suppress inactive sidebar chrome
This commit is contained in:
Mohamed Boudra
2026-07-15 09:30:57 +02:00
committed by GitHub
parent 9c40cb7637
commit 77f6069ec1
10 changed files with 326 additions and 33 deletions

View File

@@ -175,6 +175,24 @@ test.describe("Half-screen desktop layout", () => {
await expect(page.getByTestId("agent-list-backdrop")).not.toBeVisible();
});
test("keeps the left toggle center-owned without left window controls", async ({ page }) => {
await gotoAppShell(page);
const openToggle = page.getByTestId("menu-button");
const openBounds = await openToggle.locator("svg").first().boundingBox();
expect(openBounds).not.toBeNull();
expect(openBounds?.x).toBeGreaterThan(12);
await openToggle.click();
await expect(page.getByTestId("sidebar-global-new-workspace")).not.toBeVisible();
const closedToggle = page.getByTestId("menu-button");
const closedBounds = await closedToggle.locator("svg").first().boundingBox();
expect(closedBounds).not.toBeNull();
expect(closedBounds?.x).toBeCloseTo(12, 0);
expect(closedBounds?.y).toBe(openBounds?.y);
});
test("yields app navigation to the settings split", async ({ page }) => {
await gotoAppShell(page);
await page.getByTestId("sidebar-settings").click();
@@ -196,13 +214,40 @@ test.describe("Half-screen desktop layout", () => {
await expect(
page.getByTestId("explorer-tab-files").filter({ visible: true }).first(),
).toBeVisible();
await expect(page.getByTestId("workspace-explorer-toggle").first()).toBeVisible();
await expect(page.getByTestId("explorer-close")).toBeVisible();
await expect(page.getByTestId("sidebar-global-new-workspace")).not.toBeVisible();
const centerBounds = await page.getByTestId("workspace-tabs-row").first().boundingBox();
const headerGlyphBounds = await page
.getByTestId("menu-button")
.locator("svg")
.first()
.boundingBox();
const tabGlyphBounds = await page
.locator('[data-testid^="workspace-tab-"]')
.first()
.locator("svg")
.first()
.boundingBox();
expect(centerBounds).not.toBeNull();
expect(headerGlyphBounds).not.toBeNull();
expect(tabGlyphBounds).not.toBeNull();
expect((headerGlyphBounds?.x ?? 0) - (centerBounds?.x ?? 0)).toBeCloseTo(
(tabGlyphBounds?.x ?? 0) - (centerBounds?.x ?? 0),
0,
);
await expect
.poll(
async () =>
(await page.getByTestId("workspace-tabs-row").first().boundingBox())?.width ?? 0,
)
.toBeGreaterThanOrEqual(400);
await page.getByTestId("explorer-close").click();
await expect(page.getByTestId("explorer-tab-files")).not.toBeVisible();
await expect(page.getByTestId("workspace-explorer-toggle").first()).toBeVisible();
} finally {
await workspace.cleanup();
}

View File

@@ -28,6 +28,7 @@ import { QuittingOverlay } from "@/components/quitting-overlay";
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
import { AppDiagnosticHost } from "@/components/app-diagnostic-host";
import { LeftSidebar } from "@/components/left-sidebar";
import { WindowSidebarMenuToggle } from "@/components/headers/menu-header";
import { SidebarModelProvider } from "@/components/sidebar/sidebar-model";
import { CompactExplorerSidebarHost } from "@/components/compact-explorer-sidebar-host";
import { ProjectPickerModal } from "@/components/project-picker-modal";
@@ -37,9 +38,14 @@ import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog";
import { WorkspaceShortcutTargetsSubscriber } from "@/components/workspace-shortcut-targets-subscriber";
import { FloatingPanelPortalHost } from "@/components/ui/floating-panel-portal";
import { HostChooserModal, useHostChooser } from "@/hosts/host-chooser";
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
import {
getIsElectronRuntime,
HEADER_INNER_HEIGHT,
useIsCompactFormFactor,
} from "@/constants/layout";
import {
canDesktopAppSidebarShare,
resolveDesktopAppChromeLayout,
resolveDesktopAppContentMinimum,
} from "@/components/desktop-sidebar-layout";
import { isNative, isWeb } from "@/constants/platform";
@@ -95,7 +101,12 @@ import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
import { installWebScrollbarStyles } from "@/styles/install-web-scrollbar-styles";
import type { HostProfile } from "@/types/host-connection";
import { toggleDesktopSidebarsWithCheckoutIntent } from "@/utils/desktop-sidebar-toggle";
import { WindowChromeProvider, WindowChromeRegion } from "@/utils/desktop-window";
import {
useHasWindowChromeObstruction,
WindowChromeProvider,
WindowChromeRegion,
WindowChromeSafeArea,
} from "@/utils/desktop-window";
import { buildOpenProjectRoute, parseServerIdFromPathname } from "@/utils/host-routes";
import { buildNotificationRoute, resolveNotificationTarget } from "@/utils/notification-routing";
import { navigateToAgent } from "@/utils/navigate-to-agent";
@@ -403,6 +414,7 @@ interface AppContainerProps {
}
const THEME_CYCLE_ORDER: ThemeName[] = ["dark", "zinc", "midnight", "claude", "ghostty", "light"];
const WINDOW_SIDEBAR_TOGGLE_HORIZONTAL_PADDING = 12;
function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppContainerProps) {
const daemons = useHosts();
@@ -480,7 +492,12 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
requestedSidebarWidth: sidebarWidth,
viewportWidth,
});
const contentWindowChromeCorners = desktopSidebarRendered ? "top-right" : "both";
const hasTopLeftWindowControls = useHasWindowChromeObstruction("top-left");
const appChromeLayout = resolveDesktopAppChromeLayout({
desktopSidebarRendered,
hasTopLeftWindowControls,
sidebarControlsEnabled: chromeEnabled && !isFocusModeEnabled,
});
const sidebarChrome = (
<SidebarChrome
showSidebar={isCompactLayout ? chromeEnabled : desktopSidebarRendered}
@@ -490,7 +507,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
const workspaceChrome = (
<View style={rowStyle}>
{!isCompactLayout ? (
<WindowChromeRegion corners={desktopSidebarRendered ? "top-left" : "none"}>
<WindowChromeRegion corners={appChromeLayout.sidebarCorners}>
{sidebarChrome}
</WindowChromeRegion>
) : null}
@@ -501,7 +518,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
</WindowChromeRegion>
</CompactExplorerSidebarHost>
) : (
<WindowChromeRegion corners={contentWindowChromeCorners}>
<WindowChromeRegion corners={appChromeLayout.contentCorners}>
<View style={flexStyle}>{children}</View>
</WindowChromeRegion>
)}
@@ -511,6 +528,18 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
const surface = (
<View style={layoutStyles.surfaceFill}>
{workspaceChrome}
{!isCompactLayout && appChromeLayout.sidebarToggleOwner === "window" ? (
<WindowChromeRegion corners="top-left">
<WindowChromeSafeArea
placement="inline"
horizontalPadding={WINDOW_SIDEBAR_TOGGLE_HORIZONTAL_PADDING}
pointerEvents="box-none"
style={layoutStyles.windowSidebarToggle}
>
<WindowSidebarMenuToggle />
</WindowChromeSafeArea>
</WindowChromeRegion>
) : null}
<FloatingPanelPortalHost />
{isCompactLayout ? sidebarChrome : null}
<DownloadToast />
@@ -943,4 +972,15 @@ const layoutStyles = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.surface0,
},
windowSidebarToggle: {
position: "absolute",
top: 0,
left: 0,
zIndex: 20,
height: HEADER_INNER_HEIGHT,
flexDirection: "row",
alignItems: "center",
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: "transparent",
},
}));

View File

@@ -1,12 +1,59 @@
import { describe, expect, it } from "vitest";
import {
canDesktopAppSidebarShare,
resolveDesktopAppChromeLayout,
resolveDesktopAppContentMinimum,
resolveDesktopExplorerWidth,
resolveDesktopSidebarWidth,
} from "@/components/desktop-sidebar-layout";
describe("desktop sidebar layout", () => {
it("keeps the sidebar toggle window-owned beside left window controls", () => {
expect(
resolveDesktopAppChromeLayout({
desktopSidebarRendered: true,
hasTopLeftWindowControls: true,
sidebarControlsEnabled: true,
}),
).toEqual({
sidebarCorners: "top-left",
contentCorners: "top-right",
sidebarToggleOwner: "window",
});
expect(
resolveDesktopAppChromeLayout({
desktopSidebarRendered: true,
hasTopLeftWindowControls: false,
sidebarControlsEnabled: true,
}),
).toEqual({
sidebarCorners: "none",
contentCorners: "both",
sidebarToggleOwner: "content",
});
expect(
resolveDesktopAppChromeLayout({
desktopSidebarRendered: false,
hasTopLeftWindowControls: true,
sidebarControlsEnabled: true,
}),
).toEqual({
sidebarCorners: "none",
contentCorners: "both",
sidebarToggleOwner: "window",
});
});
it("hides the window-owned sidebar toggle when app chrome is suppressed", () => {
expect(
resolveDesktopAppChromeLayout({
desktopSidebarRendered: false,
hasTopLeftWindowControls: true,
sidebarControlsEnabled: false,
}).sidebarToggleOwner,
).toBe("none");
});
it("clamps a persisted wide sidebar to preserve the center pane", () => {
const atHalfScreen = resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 751 });
expect(atHalfScreen).toBe(351);

View File

@@ -8,6 +8,23 @@ import {
export const MIN_DESKTOP_CENTER_WIDTH = 400;
export function resolveDesktopAppChromeLayout(input: {
desktopSidebarRendered: boolean;
hasTopLeftWindowControls: boolean;
sidebarControlsEnabled: boolean;
}) {
const sidebarOwnsTopLeft = input.desktopSidebarRendered && input.hasTopLeftWindowControls;
let sidebarToggleOwner: "none" | "window" | "content" = "none";
if (input.sidebarControlsEnabled) {
sidebarToggleOwner = input.hasTopLeftWindowControls ? "window" : "content";
}
return {
sidebarCorners: sidebarOwnsTopLeft ? ("top-left" as const) : ("none" as const),
contentCorners: sidebarOwnsTopLeft ? ("top-right" as const) : ("both" as const),
sidebarToggleOwner,
};
}
function resolveDesktopPanelWidth(input: {
requestedWidth: number;
viewportWidth: number;

View File

@@ -30,7 +30,7 @@ import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { GitDiffPane } from "@/git/diff-pane";
import { FileExplorerPane } from "./file-explorer-pane";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { WindowChromeSafeArea } from "@/utils/desktop-window";
import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { RetainedPanelActivity } from "@/components/retained-panel";
import { isWeb } from "@/constants/platform";
@@ -130,7 +130,6 @@ export function CompactExplorerSidebar({
workspaceId={workspaceId}
workspaceRoot={workspaceRoot}
isGit={isGit}
isMobile
isOpen={isOpen}
onOpenFile={onOpenFile}
/>
@@ -225,7 +224,6 @@ export function ExplorerSidebar({
workspaceId={workspaceId}
workspaceRoot={workspaceRoot}
isGit={isGit}
isMobile={false}
isOpen={isOpen}
onOpenFile={onOpenFile}
/>
@@ -270,7 +268,6 @@ interface SidebarContentProps {
workspaceId?: string | null;
workspaceRoot: string;
isGit: boolean;
isMobile: boolean;
isOpen: boolean;
onOpenFile?: (filePath: string) => void;
}
@@ -283,13 +280,13 @@ function ExplorerSidebarContent({
workspaceId,
workspaceRoot,
isGit,
isMobile,
isOpen,
onOpenFile,
}: SidebarContentProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const toast = useToast();
const hasRightWindowControls = useHasOwnedWindowChromeObstruction("top-right");
const canQueryPullRequest = isGit && Boolean(workspaceRoot);
const prPane = usePrPaneData({
serverId,
@@ -359,9 +356,25 @@ function ExplorerSidebarContent({
)}
</View>
<View style={styles.headerRightSection}>
{isMobile && (
<Pressable onPress={onClose} style={styles.closeButton}>
<X size={18} color={theme.colors.foregroundMuted} />
{!hasRightWindowControls && (
<Pressable
onPress={onClose}
style={styles.closeButton}
testID="explorer-close"
nativeID="explorer-close"
accessible
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.explorer.close")}
hitSlop={8}
>
{({ hovered, pressed }) => (
<X
size={18}
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
)}
</Pressable>
)}
</View>

View File

@@ -5,10 +5,11 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { PanelLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { ScreenTitle } from "./screen-title";
import { HeaderToggleButton } from "./header-toggle-button";
import { HeaderToggleButton, headerIconSlotStyle } from "./header-toggle-button";
import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store";
import { useIsCompactFormFactor } from "@/constants/layout";
import { getShortcutOs } from "@/utils/shortcut-platform";
import { useHasWindowChromeObstruction, useOwnsWindowChromeCorner } from "@/utils/desktop-window";
interface MenuHeaderProps {
title?: string;
@@ -42,15 +43,18 @@ function MobileMenuIcon({ color }: { color: string }) {
);
}
export function SidebarMenuToggle({
style,
function SidebarMenuToggleButton({
isMobile,
resolvedStyle,
tooltipSide = "right",
testID = "menu-button",
nativeID = "menu-button",
}: SidebarMenuToggleProps = {}) {
}: Omit<SidebarMenuToggleProps, "style"> & {
isMobile: boolean;
resolvedStyle: StyleProp<ViewStyle>;
}) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const isMobile = useIsCompactFormFactor();
const isOpen = usePanelStore((state) => selectIsAgentListOpen(state, { isCompact: isMobile }));
const toggleAgentListForLayout = usePanelStore((state) => state.toggleAgentListForLayout);
const toggleShortcutKeys = useMemo(
@@ -58,9 +62,6 @@ export function SidebarMenuToggle({
[],
);
const menuIconColor =
!isMobile && isOpen ? theme.colors.foreground : theme.colors.foregroundMuted;
const handlePress = useCallback(() => {
toggleAgentListForLayout({ isCompact: isMobile });
}, [toggleAgentListForLayout, isMobile]);
@@ -75,21 +76,54 @@ export function SidebarMenuToggle({
tooltipSide={tooltipSide}
testID={testID}
nativeID={nativeID}
style={style}
style={resolvedStyle}
accessible
accessibilityRole="button"
accessibilityLabel={isOpen ? t("shell.menu.close") : t("shell.menu.open")}
accessibilityState={accessibilityState}
>
{isMobile ? (
<MobileMenuIcon color={menuIconColor} />
) : (
<PanelLeft size={theme.iconSize.md} color={menuIconColor} />
)}
{({ hovered, pressed }) => {
const color = hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted;
return isMobile ? (
<MobileMenuIcon color={color} />
) : (
<PanelLeft size={theme.iconSize.md} color={color} />
);
}}
</HeaderToggleButton>
);
}
export function SidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
const isMobile = useIsCompactFormFactor();
const ownsTopLeft = useOwnsWindowChromeCorner("top-left");
const hasTopLeftWindowControls = useHasWindowChromeObstruction("top-left");
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
const placeholderStyle = useMemo(
() => [headerIconSlotStyle.slot, resolvedStyle],
[resolvedStyle],
);
if (!isMobile && !ownsTopLeft) {
return null;
}
if (!isMobile && hasTopLeftWindowControls) {
return (
<View pointerEvents="none" style={placeholderStyle}>
<View style={styles.desktopMenuIconSpace} />
</View>
);
}
return <SidebarMenuToggleButton {...props} isMobile={isMobile} resolvedStyle={resolvedStyle} />;
}
export function WindowSidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
return <SidebarMenuToggleButton {...props} isMobile={false} resolvedStyle={resolvedStyle} />;
}
export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps) {
return (
<ScreenHeader
@@ -107,6 +141,12 @@ export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps)
}
const styles = StyleSheet.create((theme) => ({
leadingToggle: {
marginLeft: {
xs: 0,
md: -theme.spacing[2],
},
},
left: {
gap: theme.spacing[2],
},
@@ -116,6 +156,10 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "space-between",
alignItems: "flex-start",
},
desktopMenuIconSpace: {
width: theme.iconSize.md,
height: theme.iconSize.md,
},
mobileMenuLine: {
width: MOBILE_MENU_LINE_WIDTH,
height: MOBILE_MENU_LINE_HEIGHT,

View File

@@ -38,7 +38,7 @@ export function ScreenHeader({
const isMobile = useIsCompactFormFactor();
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
const baseHorizontalPadding = theme.spacing[2];
const baseHorizontalPadding = isMobile ? theme.spacing[2] : theme.spacing[3];
const innerStyle = useMemo(
() => [styles.inner, { paddingTop: insets.top + topPadding }],

View File

@@ -32,7 +32,7 @@ import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-disp
import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu";
import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useIsCompactFormFactor } from "@/constants/layout";
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
import { isWeb } from "@/constants/platform";
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
@@ -52,7 +52,7 @@ import { useHosts } from "@/runtime/host-runtime";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { useWorkspace } from "@/stores/session-store-hooks";
import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store";
import { WindowChromeSafeArea } from "@/utils/desktop-window";
import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window";
import { useCloseAgentListGesture } from "@/mobile-panels/gestures";
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
import {
@@ -706,6 +706,7 @@ function DesktopSidebar({
handleViewMore,
handleViewSchedules,
}: DesktopSidebarProps) {
const ownsTopLeft = useOwnsWindowChromeCorner("top-left");
const pathname = usePathname();
const hasActiveHostFilter = useSidebarViewStore((state) => state.hostFilters.length > 0);
const isSessionsActive = pathname.includes("/sessions");
@@ -759,6 +760,10 @@ function DesktopSidebar({
() => [styles.desktopSidebarBorder, { flex: 1, paddingTop: insetsTop }],
[insetsTop],
);
const sidebarHeaderGroupStyle = useMemo(
() => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome],
[ownsTopLeft],
);
const resizeHandleStyle = useMemo(
() => [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)],
[],
@@ -772,9 +777,14 @@ function DesktopSidebar({
<Animated.View style={desktopSidebarStyle}>
<View style={desktopSidebarBorderStyle}>
<View style={styles.sidebarDragArea}>
<TitlebarDragRegion />
<WindowChromeSafeArea placement="below" />
<View style={styles.sidebarHeaderGroup}>
{ownsTopLeft ? (
<View style={styles.desktopChromeRow}>
<TitlebarDragRegion />
</View>
) : (
<TitlebarDragRegion />
)}
<View style={sidebarHeaderGroupStyle}>
<SidebarNewWorkspaceHeaderRow
label={labels.newWorkspace}
testID="sidebar-global-new-workspace"
@@ -920,6 +930,9 @@ const styles = StyleSheet.create((theme) => ({
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
sidebarHeaderGroupBelowChrome: {
paddingTop: 0,
},
workspacesSectionHeader: {
flexDirection: "row",
alignItems: "center",
@@ -991,6 +1004,14 @@ const styles = StyleSheet.create((theme) => ({
sidebarDragArea: {
position: "relative",
},
desktopChromeRow: {
position: "relative",
height: HEADER_INNER_HEIGHT,
flexDirection: "row",
alignItems: "center",
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: "transparent",
},
sidebarFooter: {
flexDirection: "row",
alignItems: "center",

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
intersectWindowChromeCorners,
resolveHasOwnedWindowChromeObstruction,
resolveWindowChromeObstruction,
resolveWindowChromeSafeArea,
} from "@/utils/desktop-window";
@@ -49,4 +50,33 @@ describe("window chrome", () => {
expect(intersectWindowChromeCorners("top-right", "both")).toBe("top-right");
expect(intersectWindowChromeCorners("top-left", "top-right")).toBe("none");
});
it("reports an obstruction only when the surface owns its corner", () => {
const obstruction = {
topLeft: { width: 78, height: 45 },
topRight: { width: 140, height: 48 },
};
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction,
corners: "top-left",
corner: "top-left",
}),
).toBe(true);
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction,
corners: "top-left",
corner: "top-right",
}),
).toBe(false);
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction: { topLeft: null, topRight: null },
corners: "both",
corner: "top-right",
}),
).toBe(false);
});
});

View File

@@ -24,6 +24,8 @@ interface WindowChromeObstruction {
topRight: WindowChromeCornerObstruction | null;
}
export type WindowChromeCorner = "top-left" | "top-right";
type WindowChromeSafeAreaStyle = { height: number } | { paddingLeft: number; paddingRight: number };
const EMPTY_OBSTRUCTION: WindowChromeObstruction = { topLeft: null, topRight: null };
@@ -37,6 +39,29 @@ function windowChromeCornersFromFlags(topLeft: boolean, topRight: boolean): Wind
return "none";
}
export function windowChromeCornersInclude(
corners: WindowChromeCorners,
corner: WindowChromeCorner,
): boolean {
return corners === "both" || corners === corner;
}
export function resolveHasOwnedWindowChromeObstruction(input: {
obstruction: WindowChromeObstruction;
corners: WindowChromeCorners;
corner: WindowChromeCorner;
}): boolean {
if (!windowChromeCornersInclude(input.corners, input.corner)) return false;
return input.corner === "top-left"
? input.obstruction.topLeft !== null
: input.obstruction.topRight !== null;
}
export function useHasWindowChromeObstruction(corner: WindowChromeCorner): boolean {
const obstruction = useContext(WindowChromeContext);
return corner === "top-left" ? obstruction.topLeft !== null : obstruction.topRight !== null;
}
export function intersectWindowChromeCorners(
inherited: WindowChromeCorners,
declared: WindowChromeCorners,
@@ -210,6 +235,17 @@ export function useWindowChromeCorners(): WindowChromeCorners {
return useContext(WindowChromeCornersContext);
}
export function useOwnsWindowChromeCorner(corner: WindowChromeCorner): boolean {
const corners = useContext(WindowChromeCornersContext);
return windowChromeCornersInclude(corners, corner);
}
export function useHasOwnedWindowChromeObstruction(corner: WindowChromeCorner): boolean {
const obstruction = useContext(WindowChromeContext);
const corners = useContext(WindowChromeCornersContext);
return resolveHasOwnedWindowChromeObstruction({ obstruction, corners, corner });
}
type WindowChromeSafeAreaProps = ViewProps & {
placement: WindowChromeSafeAreaPlacement;
horizontalPadding?: number;