diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/sidebar-workspace.spec.ts index 0a7ec1a7f..2fd0f5868 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/sidebar-workspace.spec.ts @@ -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(); } diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index a61a603fa..606d4b530 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -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 = ( {!isCompactLayout ? ( - + {sidebarChrome} ) : null} @@ -501,7 +518,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon ) : ( - + {children} )} @@ -511,6 +528,18 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon const surface = ( {workspaceChrome} + {!isCompactLayout && appChromeLayout.sidebarToggleOwner === "window" ? ( + + + + + + ) : null} {isCompactLayout ? sidebarChrome : null} @@ -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", + }, })); diff --git a/packages/app/src/components/desktop-sidebar-layout.test.ts b/packages/app/src/components/desktop-sidebar-layout.test.ts index ca9d8a97f..98f226005 100644 --- a/packages/app/src/components/desktop-sidebar-layout.test.ts +++ b/packages/app/src/components/desktop-sidebar-layout.test.ts @@ -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); diff --git a/packages/app/src/components/desktop-sidebar-layout.ts b/packages/app/src/components/desktop-sidebar-layout.ts index e71b42273..fc0a07d82 100644 --- a/packages/app/src/components/desktop-sidebar-layout.ts +++ b/packages/app/src/components/desktop-sidebar-layout.ts @@ -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; diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index c691332e0..942818b97 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -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({ )} - {isMobile && ( - - + {!hasRightWindowControls && ( + + {({ hovered, pressed }) => ( + + )} )} diff --git a/packages/app/src/components/headers/menu-header.tsx b/packages/app/src/components/headers/menu-header.tsx index aea0abf2e..40feafb15 100644 --- a/packages/app/src/components/headers/menu-header.tsx +++ b/packages/app/src/components/headers/menu-header.tsx @@ -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 & { + isMobile: boolean; + resolvedStyle: StyleProp; +}) { 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 ? ( - - ) : ( - - )} + {({ hovered, pressed }) => { + const color = hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted; + return isMobile ? ( + + ) : ( + + ); + }} ); } +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 ( + + + + ); + } + + return ; +} + +export function WindowSidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) { + const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]); + return ; +} + export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps) { return ( ({ + 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, diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index b8f078e15..c2bd2f5d3 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -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 }], diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 25b957ed8..e1e493142 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -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({ - - - + {ownsTopLeft ? ( + + + + ) : ( + + )} + ({ 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", diff --git a/packages/app/src/utils/desktop-window.test.ts b/packages/app/src/utils/desktop-window.test.ts index 27de3f1ab..c9f915905 100644 --- a/packages/app/src/utils/desktop-window.test.ts +++ b/packages/app/src/utils/desktop-window.test.ts @@ -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); + }); }); diff --git a/packages/app/src/utils/desktop-window.tsx b/packages/app/src/utils/desktop-window.tsx index 4af5e8733..d3c6a5cfd 100644 --- a/packages/app/src/utils/desktop-window.tsx +++ b/packages/app/src/utils/desktop-window.tsx @@ -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;