From 6cd5b49c13f5cca1bcfe8b64ea0bbeaace45c244 Mon Sep 17 00:00:00 2001 From: lbb00 Date: Tue, 8 Sep 2026 13:08:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(devtools):=20=E7=BB=9F=E4=B8=80=20WebView?= =?UTF-8?q?=20=E7=9A=84=E5=BA=95=E9=83=A8=E5=AE=89=E5=85=A8=E5=8C=BA?= =?UTF-8?q?=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devtools/docs/ios-safe-area-and-notch.md | 8 +- .../docs/simulator-render-architecture.md | 10 +-- .../e2e/window-info-follows-device.spec.ts | 19 ++--- .../src/main/services/safe-area/index.test.ts | 82 ++++++++++--------- .../src/main/services/safe-area/index.ts | 44 ++++------ .../services/views/native-simulator-view.ts | 15 ++-- 6 files changed, 83 insertions(+), 95 deletions(-) diff --git a/packages/devtools/docs/ios-safe-area-and-notch.md b/packages/devtools/docs/ios-safe-area-and-notch.md index a40b7f1d..0ecc23a3 100644 --- a/packages/devtools/docs/ios-safe-area-and-notch.md +++ b/packages/devtools/docs/ios-safe-area-and-notch.md @@ -53,11 +53,11 @@ devtools 只用其中几个入口: `env(safe-area-inset-*)` 由 UA 定义,作者样式改不了,所以走 CDP。`src/main/services/safe-area/index.ts` 在 simulator WCV 的 `did-attach-webview` 时对每个 render-host guest 发 `Emulation.setSafeAreaInsetsOverride`,这是 guest `WebContents` 可用的最早时刻,页面还没绘制。 - `wc.debugger` 会话不归 safe-area 管,走共享的 `CdpSessionBroker`(`src/main/services/cdp-session/index.ts`)。`wc.debugger` 是单 owner API,没有 broker 时多个消费者会互相抢会话。safe-area 每个 guest 拿一个 `CdpSessionLease`,在上面 `send('Emulation.setSafeAreaInsetsOverride', { insets })`。`insets` 带全部 8 个字段(`top/topMax/right/rightMax/bottom/bottomMax/left/leftMax`,base 等于 max),漏掉 `*Max` 会让 `env(safe-area-max-inset-*)` 停在 0。 -- 每个 guest 的页面策略(`isTabPage` 来自 URL 的 `isTab=1`,`isCustomNav` 来自 `navStyle=custom`,两者都由 `dmb-resource-url.ts` 按页面的 `windowConfig` 写进 render-host URL)在 `will-attach-webview` 时读出并存下(`parseGuestPageInsetPolicy`)(`did-attach` 时 `getURL()` 还是空),设备切换重发时复用;guest `destroyed` 时清掉。lease 在 broker `onDetach` 时丢弃,下次 override 重新申请。 +- 每个 guest 的页面策略只有 `isCustomNav`,来自 URL 的 `navStyle=custom`(由 `dmb-resource-url.ts` 按页面的 `windowConfig` 写进 render-host URL)。它在 `will-attach-webview` 时读出并存下(`parseGuestPageInsetPolicy`;`did-attach` 时 `getURL()` 还是空),设备切换重发时复用;guest `destroyed` 时清掉。lease 在 broker `onDetach` 时丢弃,下次 override 重新申请。 - **重发时机**:(1) guest attach(页面栈新页面),(2) 设备或横竖屏切换(对所有已 attach 的 guest 重发)。 -- **只注入 webview 真正贴着的边**,页面自己的 `env()` padding 不会和外壳已覆盖的区域重复计算(`guestInsets()`): +- **按页面导航样式处理顶部,底部始终使用设备值**(`guestInsets()`): - `top`:自定义导航栏页(`navigationStyle: custom`,页面全出血到屏幕顶部)取设备当前方向的 `safeAreaInsets.top`;默认导航栏页为 0,因为 webview 本来就从外壳导航栏下方开始,三端 native 也是这样。 - - `bottom`:tab 页为 0(外壳 tabBar 的背景延伸到底部内边距,页面内容不贴底);非 tab 页取 `safeAreaInsets.bottom`(页面全出血到设备底部,自己用 `env(safe-area-inset-bottom)` 避让)。 + - `bottom`:所有 render WebView 都取设备当前方向的 `safeAreaInsets.bottom`;tabBar 是否存在不改变页面的 `env(safe-area-inset-bottom)`。 - `left` / `right`:直接取设备当前方向的 `safeAreaInsets.left/right`,横屏灵动岛机型不再是 0。 - **`webContents.debugger` 独占**。外部工具(`--remote-debugging-port`)已经 attach 时 `attach()` 会抛错,只记警告、内边距保持 0,没有纯 CSS 回退。 @@ -68,7 +68,7 @@ Home 指示条由 frame 画,是绝对定位的透明覆盖层,不占布局 - tab 页:外壳 tabBar 的背景延伸过底部内边距(`padding-bottom = safeAreaInsets.bottom`,`tab-bar.tsx`),指示条压在 tabBar 颜色上。 - 非 tab 页:页面 webview 全出血到设备底部,指示条压在页面内容上。 -因为 tab 页已经由外壳让出底部,其 `env(safe-area-inset-bottom)` 被覆盖成 0,避免页面重复避让。 +tab 页的外壳 tabBar 仍会延伸到底部内边距;同时,该页 WebView 的 `env(safe-area-inset-bottom)` 保持设备值,和其他 render WebView 一致。 ## JS `safeArea` diff --git a/packages/devtools/docs/simulator-render-architecture.md b/packages/devtools/docs/simulator-render-architecture.md index 9285a77c..b2a0b8e7 100644 --- a/packages/devtools/docs/simulator-render-architecture.md +++ b/packages/devtools/docs/simulator-render-architecture.md @@ -228,11 +228,11 @@ service host. Two things keep it working: The DeviceShell's `.device-shell__home-indicator` strip is sized to `device.safeAreaInsets.bottom` (gesture-bar devices only; home-button SE-class -has bottom inset 0). The page's `env(safe-area-inset-bottom)` is injected as 0 — -the shell reserves the bottom, so there is a single bottom-inset mechanism. The -top safe area reaches the page via the CDP `setSafeAreaInsetsOverride`; the notch -is an in-screen occluder drawn by the DeviceShell's status bar. (See the -safe-area doc for the CDP wiring.) +has bottom inset 0). Every render WebView receives that same value as +`env(safe-area-inset-bottom)` through the CDP `setSafeAreaInsetsOverride`; a tab +bar does not change the injected value. The top safe area still depends on the +page navigation style. The notch is an in-screen occluder drawn by the +DeviceShell's status bar. (See the safe-area doc for the CDP wiring.) ## Key files diff --git a/packages/devtools/e2e/window-info-follows-device.spec.ts b/packages/devtools/e2e/window-info-follows-device.spec.ts index a16bf3ff..5ee22fa0 100644 --- a/packages/devtools/e2e/window-info-follows-device.spec.ts +++ b/packages/devtools/e2e/window-info-follows-device.spec.ts @@ -67,11 +67,9 @@ const IPHONE_15_WINDOW_INFO = { screenTop: 54, } /** env(safe-area-inset-*) the fixture's pages should see on iPhone 15 - * portrait: default-nav pages start BELOW the navigation bar so their top - * inset is already consumed (I4); the bottom inset only applies where no tab - * bar covers it. */ -const DEFAULT_NAV_TAB_INSETS = { top: '0px', bottom: '0px' } -const DEFAULT_NAV_NON_TAB_INSETS = { top: '0px', bottom: '34px' } + * portrait: default-nav pages start below the navigation bar, so their top + * inset is already consumed; every render WebView gets the device bottom inset. */ +const DEFAULT_NAV_INSETS = { top: '0px', bottom: '34px' } const CUSTOM_NAV_INSETS = { top: '59px', bottom: '34px' } /** The fields the three code paths (sync binding, async simulator handler, @@ -344,21 +342,20 @@ test.describe('window info follows the selected device', () => { expect(windowFieldsOf(success!)).toEqual(IPHONE_15_SHARED_FIELDS) }) - test('4. default-nav pages see no top inset; bottom inset only without a tab bar', async () => { + test('4. default-nav pages see no top inset and the device bottom inset', async () => { // The tab page the app booted into. const home = await guestShowing(electronApp, 'HOME PAGE') expect( { top: home.probeTop, bottom: home.probeBottom }, - 'tab page with the default navigation bar: guest already starts below the nav bar, ' - + 'and the tab bar covers the home indicator', - ).toEqual(DEFAULT_NAV_TAB_INSETS) + 'tab page with the default navigation bar: the guest starts below the nav bar', + ).toEqual(DEFAULT_NAV_INSETS) await navigateTo(electronApp, '/pages/detail/detail') const detail = await guestShowing(electronApp, 'DETAIL PAGE') expect( { top: detail.probeTop, bottom: detail.probeBottom }, - 'non-tab page with the default navigation bar: no top inset, but the home indicator is exposed', - ).toEqual(DEFAULT_NAV_NON_TAB_INSETS) + 'non-tab page with the default navigation bar gets the same device bottom inset', + ).toEqual(DEFAULT_NAV_INSETS) }) test('5. a navigationStyle:custom page sees the full top inset', async () => { diff --git a/packages/devtools/src/main/services/safe-area/index.test.ts b/packages/devtools/src/main/services/safe-area/index.test.ts index 6850fb4d..38e05955 100644 --- a/packages/devtools/src/main/services/safe-area/index.test.ts +++ b/packages/devtools/src/main/services/safe-area/index.test.ts @@ -14,19 +14,15 @@ import { describe, it, expect, vi } from 'vitest' import type { WebContents } from 'electron' import { createConnectionRegistry } from '@dimina-kit/electron-deck/main' -import { createSafeAreaController } from './index.js' +import { createSafeAreaController, parseGuestPageInsetPolicy } from './index.js' import { createCdpSessionBroker } from '../cdp-session/index.js' type AnyFn = (...args: unknown[]) => unknown /** Minimal emitter-backed WebContents fake (id/once/emit/isDestroyed + the * debugger surface safe-area touches). `sink` captures every `sendCommand`. */ -// The broker (see cdp-session/index.ts, which safe-area now goes through -// instead of touching wc.debugger directly) reads isAttached()/on()/ -// removeListener() in addition to attach()/detach()/sendCommand() — this fake -// grows the same surface. Existing assertions (connection-routed teardown, -// per-page-type bottom inset) are unchanged; only the mock's surface area -// needed to widen to match the broker's dependency. +// The broker reads isAttached()/on()/removeListener() in addition to +// attach()/detach()/sendCommand(), so this fake provides that debugger surface. function makeWc( id: number, sink?: Array<{ method: string; params: unknown }>, @@ -77,7 +73,7 @@ describe('createSafeAreaController teardown routing', () => { const controller = createSafeAreaController({ connections }) const wc = makeWc(7) - controller.applyToGuest(wc, null, { isTabPage: false, isCustomNav: false }) + controller.applyToGuest(wc, null, { isCustomNav: false }) // The connection was acquired for this guest. expect(connections.get(wc.id), 'guest connection must be live before destroy').toBeDefined() @@ -100,42 +96,52 @@ describe('createSafeAreaController teardown routing', () => { }) }) -describe('createSafeAreaController per-page-type bottom inset', () => { +describe('createSafeAreaController bottom inset', () => { function lastInsets(sink: Array<{ method: string; params: unknown }>) { const call = [...sink].reverse().find((c) => c.method === 'Emulation.setSafeAreaInsetsOverride') return (call?.params as { insets: { top: number; bottom: number; bottomMax: number } }).insets } - it('a non-tab page gets the real bottom inset (page opts in via env)', () => { + it('forwards the real bottom inset to a render guest', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(1, sink), DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(makeWc(1, sink), DEVICE, { isCustomNav: true }) const insets = lastInsets(sink) expect(insets.top).toBe(47) expect(insets.bottom).toBe(34) expect(insets.bottomMax).toBe(34) }) - it('a tab page gets bottom 0 (the shell tabBar fills the safe area)', () => { + it('keeps the device bottom inset for a policy parsed from a tab-page URL', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(2, sink), DEVICE, { isTabPage: true, isCustomNav: true }) + const policy = parseGuestPageInsetPolicy('https://example.test/render-host?isTab=1&navStyle=default') + + controller.applyToGuest(makeWc(18, sink), DEVICE, policy) + + expect(lastInsets(sink).bottom).toBe(34) + }) + + it('forwards the same real bottom inset to another render guest', () => { + const sink: Array<{ method: string; params: unknown }> = [] + const controller = createSafeAreaController() + controller.applyToGuest(makeWc(2, sink), DEVICE, { isCustomNav: true }) const insets = lastInsets(sink) expect(insets.top).toBe(47) - expect(insets.bottom).toBe(0) - expect(insets.bottomMax).toBe(0) + expect(insets.bottom).toBe(34) + expect(insets.bottomMax).toBe(34) }) - it('reapplyAll keeps each guest its attached page type', () => { + it('reapplyAll keeps the device bottom inset for every guest', () => { const sinkTab: Array<{ method: string; params: unknown }> = [] const sinkPage: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(3, sinkTab), DEVICE, { isTabPage: true, isCustomNav: true }) - controller.applyToGuest(makeWc(4, sinkPage), DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(makeWc(3, sinkTab), DEVICE, { isCustomNav: true }) + controller.applyToGuest(makeWc(4, sinkPage), DEVICE, { isCustomNav: true }) sinkTab.length = 0 sinkPage.length = 0 controller.reapplyAll(DEVICE) - expect(lastInsets(sinkTab).bottom).toBe(0) + expect(lastInsets(sinkTab).bottom).toBe(34) expect(lastInsets(sinkPage).bottom).toBe(34) }) @@ -147,7 +153,7 @@ describe('createSafeAreaController per-page-type bottom inset', () => { const sink: Array<{ method: string; params: unknown }> = [] const wc = makeWc(5, sink) const controller = createSafeAreaController() - controller.applyToGuest(wc, DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(wc, DEVICE, { isCustomNav: true }) expect(lastInsets(sink).bottom).toBe(34) // Something outside safe-area detaches the shared debugger session @@ -156,7 +162,7 @@ describe('createSafeAreaController per-page-type bottom inset', () => { sink.length = 0 // reapplyAll must reacquire (not silently no-op on a stale lease) and - // keep applying the SAME page-type policy this guest attached with. + // keep applying the same navigation-style policy this guest attached with. controller.reapplyAll(DEVICE) expect(sink.length).toBeGreaterThan(0) expect(lastInsets(sink).bottom).toBe(34) @@ -180,7 +186,7 @@ describe('createSafeAreaController per-edge left/right insets', () => { it('forwards the device safeAreaInsets right/left into the CDP override', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(8, sink), LANDSCAPE_DEVICE, { isTabPage: false, isCustomNav: false }) + controller.applyToGuest(makeWc(8, sink), LANDSCAPE_DEVICE, { isCustomNav: false }) const insets = lastFullInsets(sink) expect(insets.right).toBe(59) expect(insets.rightMax).toBe(59) @@ -188,12 +194,12 @@ describe('createSafeAreaController per-edge left/right insets', () => { expect(insets.leftMax).toBe(59) }) - it('a tab page still gets bottom 0 but keeps the real left/right insets', () => { + it('forwards the device bottom and left/right insets to a render guest', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(9, sink), LANDSCAPE_DEVICE, { isTabPage: true, isCustomNav: false }) + controller.applyToGuest(makeWc(9, sink), LANDSCAPE_DEVICE, { isCustomNav: false }) const insets = lastFullInsets(sink) - expect(insets.bottom).toBe(0) + expect(insets.bottom).toBe(21) expect(insets.left).toBe(59) expect(insets.right).toBe(59) }) @@ -203,7 +209,7 @@ describe('createSafeAreaController broker ownership', () => { it('disposes a private (non-injected) broker on dispose(), detaching self-attached sessions', () => { const wc = makeWc(6) const controller = createSafeAreaController() // no broker injected -> owns a private one - controller.applyToGuest(wc, null, { isTabPage: false, isCustomNav: false }) + controller.applyToGuest(wc, null, { isCustomNav: false }) expect(wc.debugger.attach).toHaveBeenCalled() controller.dispose() @@ -215,7 +221,7 @@ describe('createSafeAreaController broker ownership', () => { const broker = createCdpSessionBroker() const wc = makeWc(7) const controller = createSafeAreaController({ broker }) - controller.applyToGuest(wc, null, { isTabPage: false, isCustomNav: false }) + controller.applyToGuest(wc, null, { isCustomNav: false }) expect(wc.debugger.attach).toHaveBeenCalled() controller.dispose() @@ -241,7 +247,7 @@ describe('createSafeAreaController per-page navigation-style top inset', () => { it('a default navigation-bar page gets top 0 (the shell nav bar already clears the notch)', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(10, sink), DEVICE, { isTabPage: false, isCustomNav: false }) + controller.applyToGuest(makeWc(10, sink), DEVICE, { isCustomNav: false }) const insets = lastInsets(sink) expect(insets.top).toBe(0) expect(insets.topMax).toBe(0) @@ -250,26 +256,26 @@ describe('createSafeAreaController per-page navigation-style top inset', () => { it('a custom navigation-bar page gets the real device top inset', () => { const sink: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(11, sink), DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(makeWc(11, sink), DEVICE, { isCustomNav: true }) const insets = lastInsets(sink) expect(insets.top).toBe(47) expect(insets.topMax).toBe(47) }) - it('the bottom inset stays page-type driven regardless of navigation style', () => { + it('the device bottom inset is the same for every navigation style', () => { const defaultNavTab: Array<{ method: string; params: unknown }> = [] const defaultNavPage: Array<{ method: string; params: unknown }> = [] const customNavTab: Array<{ method: string; params: unknown }> = [] const customNavPage: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(12, defaultNavTab), DEVICE, { isTabPage: true, isCustomNav: false }) - controller.applyToGuest(makeWc(13, defaultNavPage), DEVICE, { isTabPage: false, isCustomNav: false }) - controller.applyToGuest(makeWc(14, customNavTab), DEVICE, { isTabPage: true, isCustomNav: true }) - controller.applyToGuest(makeWc(15, customNavPage), DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(makeWc(12, defaultNavTab), DEVICE, { isCustomNav: false }) + controller.applyToGuest(makeWc(13, defaultNavPage), DEVICE, { isCustomNav: false }) + controller.applyToGuest(makeWc(14, customNavTab), DEVICE, { isCustomNav: true }) + controller.applyToGuest(makeWc(15, customNavPage), DEVICE, { isCustomNav: true }) - expect(lastInsets(defaultNavTab).bottom).toBe(0) + expect(lastInsets(defaultNavTab).bottom).toBe(34) expect(lastInsets(defaultNavPage).bottom).toBe(34) - expect(lastInsets(customNavTab).bottom).toBe(0) + expect(lastInsets(customNavTab).bottom).toBe(34) expect(lastInsets(customNavPage).bottom).toBe(34) }) @@ -277,8 +283,8 @@ describe('createSafeAreaController per-page navigation-style top inset', () => { const defaultNav: Array<{ method: string; params: unknown }> = [] const customNav: Array<{ method: string; params: unknown }> = [] const controller = createSafeAreaController() - controller.applyToGuest(makeWc(16, defaultNav), DEVICE, { isTabPage: false, isCustomNav: false }) - controller.applyToGuest(makeWc(17, customNav), DEVICE, { isTabPage: false, isCustomNav: true }) + controller.applyToGuest(makeWc(16, defaultNav), DEVICE, { isCustomNav: false }) + controller.applyToGuest(makeWc(17, customNav), DEVICE, { isCustomNav: true }) defaultNav.length = 0 customNav.length = 0 diff --git a/packages/devtools/src/main/services/safe-area/index.ts b/packages/devtools/src/main/services/safe-area/index.ts index 8cc330de..1e693d06 100644 --- a/packages/devtools/src/main/services/safe-area/index.ts +++ b/packages/devtools/src/main/services/safe-area/index.ts @@ -20,15 +20,10 @@ import { createCdpSessionBroker, type CdpSessionBroker, type CdpSessionLease } f * device inset here would push the page content down a second time. * - custom navigation bar → the guest is full-bleed to the device top and * borders the unsafe zone itself → the real TOP inset. - * The BOTTOM inset is per page TYPE (WeChat parity): - * - tab page → the shell draws the tabBar and extends its background through - * the home-indicator safe area; the guest (page content sits ABOVE the - * tabBar) does not border the bottom unsafe zone → BOTTOM 0. - * - non-tab page → the guest is full-bleed to the device bottom, so surface - * the real bottom inset and let the page opt in via its own - * `env(safe-area-inset-bottom)`; the shell reserves nothing there. - * Both come off the attaching guest's render-host URL (`isTab`, `navStyle`), - * read in view-manager's `did-attach-webview`. + * The BOTTOM inset always comes from the device. A render WebView exposes the + * same bottom `env(safe-area-inset-bottom)` for a device whether or not it is + * a tab page. The navigation style comes from the attaching guest's render-host + * URL (`navStyle`), read in view-manager's `will-attach-webview`. * (Design doc: docs/ios-safe-area-and-notch.md.) */ @@ -46,27 +41,22 @@ interface CdpSafeAreaInsets { } /** What the guest's own page contributes to the inset policy: whether the - * shell draws a tabBar under it, and whether it draws a navigation bar over - * the notch for it. */ + * shell draws a navigation bar over the notch for it. */ export interface GuestPageInsetPolicy { - isTabPage: boolean isCustomNav: boolean } /** - * Read a guest's inset policy off its render-host URL (`isTab`, `navStyle` — - * both written by `buildRenderHostDocumentUrl`). An unparseable URL degrades to - * the default-nav, non-tab policy rather than failing the attach. + * Read a guest's inset policy off its render-host URL (`navStyle`, written by + * `buildRenderHostDocumentUrl`). An unparseable URL degrades to the default-nav + * policy rather than failing the attach. */ export function parseGuestPageInsetPolicy(src: string): GuestPageInsetPolicy { try { const params = new URL(src).searchParams - return { - isTabPage: params.get('isTab') === '1', - isCustomNav: params.get('navStyle') === 'custom', - } + return { isCustomNav: params.get('navStyle') === 'custom' } } catch { - return { isTabPage: false, isCustomNav: false } + return { isCustomNav: false } } } @@ -75,13 +65,9 @@ function guestInsets(device: NativeDeviceInfo | null, page: GuestPageInsetPolicy // default-nav page already starts below the shell nav bar, which covers the // notch itself. const top = page.isCustomNav ? (device?.safeAreaInsets.top ?? 0) : 0 - // A tab page's content sits above the shell-drawn tabBar (which fills the - // bottom safe area), so it never borders the bottom unsafe zone. A non-tab - // page is full-bleed to the device bottom, so surface the real inset for its - // own `env(safe-area-inset-bottom)` opt-in. - const bottom = page.isTabPage ? 0 : (device?.safeAreaInsets.bottom ?? 0) + const bottom = device?.safeAreaInsets.bottom ?? 0 // Left/right come straight from the device (e.g. a landscape Dynamic Island - // rotates into a side notch) — unlike top/bottom they don't depend on page + // rotates into a side notch) — unlike top they don't depend on page // type, since neither shell chrome nor the tabBar reserves horizontal space. const right = device?.safeAreaInsets.right ?? 0 const left = device?.safeAreaInsets.left ?? 0 @@ -91,9 +77,9 @@ function guestInsets(device: NativeDeviceInfo | null, page: GuestPageInsetPolicy export interface SafeAreaController { /** Attach the debugger to a freshly-attached render-host guest and push the * current device's insets. `page` selects the top policy (real inset only for - * a custom-nav page) and the bottom policy (0 for tab pages, the real inset - * for full-bleed non-tab pages). No-op (warn) if the guest is already claimed - * by an external CDP client — env then stays 0. */ + * a custom-nav page); the bottom inset always comes from the device. No-op + * (warn) if the guest is already claimed by an external CDP client — env + * then stays 0. */ applyToGuest(guestWc: WebContents, device: NativeDeviceInfo | null, page: GuestPageInsetPolicy): void /** Re-push insets to every still-attached guest after a device change (each * guest keeps the page policy it attached with). */ diff --git a/packages/devtools/src/main/services/views/native-simulator-view.ts b/packages/devtools/src/main/services/views/native-simulator-view.ts index 68ba51c6..b71c259e 100644 --- a/packages/devtools/src/main/services/views/native-simulator-view.ts +++ b/packages/devtools/src/main/services/views/native-simulator-view.ts @@ -281,8 +281,8 @@ export function createNativeSimulatorView( // them with contextIsolation/sandbox off so the render runtime + its preload // share the page realm. (A top-level WebContentsView can host these guests; a // `` guest cannot — that's the whole point of Option A.) - // Inset policy (`isTab`, `navStyle`) of each attaching guest, captured from - // its render-host URL in will-attach (where `params.src` carries the full + // Inset policy (`navStyle`) of each attaching guest, captured from its + // render-host URL in will-attach (where `params.src` carries the full // URL) and consumed FIFO in the matching did-attach — `guestWc.getURL()` is // still empty there. // Per-attach scope: a fresh simWc + handlers are built on every (re)attach. @@ -309,12 +309,11 @@ export function createNativeSimulatorView( guestWc.setZoomFactor(currentZoomFactor) } catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ } // Simulate this device's CSS env(safe-area-inset-*) on the fresh guest - // before it paints, so notch-aware page layout resolves correctly. Both - // the top and the bottom inset depend on the page (see services/safe-area): - // only a custom-nav page borders the unsafe top zone, and only a non-tab - // page borders the bottom one. The policy was captured from the - // render-host URL in will-attach (FIFO). - const guestPage = pendingGuestPages.shift() ?? { isTabPage: false, isCustomNav: false } + // before it paints, so notch-aware page layout resolves correctly. Only + // the top inset depends on the page (see services/safe-area): only a + // custom-nav page borders the unsafe top zone. The policy was captured + // from the render-host URL in will-attach (FIFO). + const guestPage = pendingGuestPages.shift() ?? { isCustomNav: false } safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, guestPage) // Page-level resource loads (images/fonts/page fetch) run in THIS guest's // network stack, never the simulator's — without this, only wx.request