@@ -647,6 +647,22 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
647647 if ( Option . isSome ( next ) ) yield * emit ( tabId , next . value ) ;
648648 } ) ;
649649
650+ /**
651+ * Pushes a tab's zoom factor onto whichever guest it currently owns, reading
652+ * both at call time. Anything that applies zoom after an await goes through
653+ * here: a snapshot taken before the await can be older than a zoom action that
654+ * landed in between, and re-applying it would roll that action back.
655+ */
656+ const assertTabZoom = Effect . fn ( "PreviewManager.assertTabZoom" ) ( function * ( tabId : string ) {
657+ const tab = ( yield * SynchronizedRef . get ( tabsRef ) ) . get ( tabId ) ;
658+ if ( ! tab || tab . webContentsId == null ) return ;
659+ const wc = webContents . fromId ( tab . webContentsId ) ;
660+ if ( ! wc || wc . isDestroyed ( ) ) return ;
661+ yield * attempt ( { operation : "assertTabZoom" , tabId, webContentsId : wc . id } , ( ) =>
662+ wc . setZoomFactor ( tab . zoomFactor ) ,
663+ ) . pipe ( Effect . ignore ) ;
664+ } ) ;
665+
650666 const requireWebContents = Effect . fn ( "PreviewManager.requireWebContents" ) ( function * (
651667 tabId : string ,
652668 ) {
@@ -1305,10 +1321,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
13051321 confirmedNavigation = false ,
13061322 ) {
13071323 if ( wc . isDestroyed ( ) ) return ;
1308- const zoomFactor = yield * attempt (
1309- { operation : "syncWebContentsState.getZoomFactor" , tabId, webContentsId : wc . id } ,
1310- ( ) => wc . getZoomFactor ( ) ,
1311- ) . pipe ( Effect . option ) ;
13121324 const computedNavStatus = computeNavStatus ( wc ) ;
13131325 const canGoBack = wc . navigationHistory . canGoBack ( ) ;
13141326 const canGoForward = wc . navigationHistory . canGoForward ( ) ;
@@ -1338,7 +1350,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
13381350 navStatus,
13391351 canGoBack,
13401352 canGoForward,
1341- ...( Option . isSome ( zoomFactor ) ? { zoomFactor : zoomFactor . value } : { } ) ,
1353+ // zoomFactor is deliberately not read back from the guest: Chromium
1354+ // reports the level it inherited from the app window, so mirroring it
1355+ // would turn an app zoom into the preview's own zoom.
13421356 updatedAt,
13431357 } ;
13441358 return [
@@ -1716,11 +1730,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
17161730 const annotationTheme = yield * Ref . get ( annotationThemeRef ) ;
17171731 const currentAttachment = attached . get ( webContentsId ) ;
17181732 if ( tab . webContentsId === webContentsId && currentAttachment ?. webContents === wc ) {
1719- const zoomFactor = yield * attempt (
1720- { operation : "registerWebview.getZoomFactor" , tabId, webContentsId } ,
1721- ( ) => wc . getZoomFactor ( ) ,
1722- ) ;
1723- yield * update ( tabId , { zoomFactor } ) ;
1733+ // The guest we already own re-announced itself, so nothing about the tab
1734+ // changed. Only push its zoom back down — Chromium may have just handed
1735+ // this guest the app window's zoom level.
1736+ yield * assertTabZoom ( tabId ) ;
17241737 yield * attempt ( { operation : "registerWebview.sendTheme" , tabId, webContentsId } , ( ) =>
17251738 wc . send ( ANNOTATION_THEME_CHANNEL , annotationTheme ) ,
17261739 ) ;
@@ -1749,18 +1762,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
17491762 ) {
17501763 return yield * new PreviewTabNotFoundError ( { tabId } ) ;
17511764 }
1752- const zoomFactor =
1753- replacedWebContentsId !== null
1754- ? yield * attempt (
1755- { operation : "registerWebview.restoreZoomFactor" , tabId, webContentsId } ,
1756- ( ) => {
1757- wc . setZoomFactor ( currentTab . zoomFactor ) ;
1758- return currentTab . zoomFactor ;
1759- } ,
1760- )
1761- : yield * attempt ( { operation : "registerWebview.getZoomFactor" , tabId, webContentsId } , ( ) =>
1762- wc . getZoomFactor ( ) ,
1763- ) ;
1765+ // Always assert the tab's own zoom rather than reading the guest's: a guest
1766+ // attaching while the app UI is zoomed starts at the embedder's inherited
1767+ // zoom level, which is not the preview's zoom. Done before the guest is
1768+ // published so it never paints a frame at the inherited zoom.
1769+ yield * attempt ( { operation : "registerWebview.restoreZoomFactor" , tabId, webContentsId } , ( ) =>
1770+ wc . setZoomFactor ( currentTab . zoomFactor ) ,
1771+ ) ;
17641772 yield * attachListeners ( tabId , wc ) ;
17651773 const registeredAt = yield * currentIso ;
17661774 const registration = yield * SynchronizedRef . modifyEffect ( tabsRef , ( tabs ) =>
@@ -1784,7 +1792,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
17841792 navStatus : pendingUrl === null ? computeNavStatus ( wc ) : current . navStatus ,
17851793 canGoBack : wc . navigationHistory . canGoBack ( ) ,
17861794 canGoForward : wc . navigationHistory . canGoForward ( ) ,
1787- zoomFactor,
17881795 updatedAt : registeredAt ,
17891796 } ;
17901797 return [
@@ -1806,6 +1813,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
18061813 return yield * new PreviewTabNotFoundError ( { tabId } ) ;
18071814 }
18081815 const { state : registered , pendingUrl } = registration . value ;
1816+ // A zoom action that landed while this attach was in flight addressed the
1817+ // guest this one replaced, so settle the new guest on the committed factor.
1818+ yield * assertTabZoom ( tabId ) ;
18091819 runFork ( restoreControlSession ( tabId , wc ) ) ;
18101820 yield * emit ( tabId , registered ) ;
18111821 yield * attempt ( { operation : "registerWebview.sendTheme" , tabId, webContentsId } , ( ) =>
@@ -2099,6 +2109,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
20992109 ) ;
21002110 } ) ;
21012111
2112+ /**
2113+ * Chromium hands every guest `<webview>` the embedder's zoom level, so zooming
2114+ * the app UI drags the previewed page along with it. The preview browser owns
2115+ * its own zoom factor, so re-assert it on each attached guest whenever the main
2116+ * window's zoom changes (see DesktopWindow.zoomMain).
2117+ */
2118+ const reapplyZoom = Effect . fn ( "PreviewManager.reapplyZoom" ) ( function * ( ) {
2119+ const tabIds = Array . from ( ( yield * SynchronizedRef . get ( tabsRef ) ) . keys ( ) ) ;
2120+ yield * Effect . forEach ( tabIds , assertTabZoom , { discard : true } ) ;
2121+ } ) ;
2122+
21022123 const applyZoom = Effect . fn ( "PreviewManager.applyZoom" ) ( function * (
21032124 tabId : string ,
21042125 transform : ( current : number ) => number ,
@@ -3476,6 +3497,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
34763497 openPictureInPicture,
34773498 openDevTools,
34783499 pickElement,
3500+ reapplyZoom,
34793501 refresh,
34803502 registerWebview,
34813503 resetZoom : ( tabId : string ) => applyZoom ( tabId , ( ) => DEFAULT_ZOOM_FACTOR ) ,
@@ -3774,6 +3796,9 @@ export class PreviewManager extends Context.Service<
37743796 readonly zoomIn : ( tabId : string ) => Effect . Effect < void , PreviewManagerError > ;
37753797 readonly zoomOut : ( tabId : string ) => Effect . Effect < void , PreviewManagerError > ;
37763798 readonly resetZoom : ( tabId : string ) => Effect . Effect < void , PreviewManagerError > ;
3799+ // Re-applies every attached guest's own zoom factor, undoing the zoom level
3800+ // Chromium inherits from the embedder when the app UI zooms.
3801+ readonly reapplyZoom : ( ) => Effect . Effect < void > ;
37773802 readonly hardReload : ( tabId : string ) => Effect . Effect < void , PreviewManagerError > ;
37783803 readonly setColorScheme : (
37793804 tabId : string ,
@@ -3874,6 +3899,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
38743899 zoomIn : operations . zoomIn ,
38753900 zoomOut : operations . zoomOut ,
38763901 resetZoom : operations . resetZoom ,
3902+ reapplyZoom : operations . reapplyZoom ,
38773903 hardReload : operations . hardReload ,
38783904 setColorScheme : operations . setColorScheme ,
38793905 openDevTools : operations . openDevTools ,
0 commit comments