@@ -6,14 +6,19 @@ import type {
66 ScopedThreadRef ,
77} from "@t3tools/contracts" ;
88import { useShallow } from "zustand/react/shallow" ;
9- import { useCallback , useEffect , useRef , useState } from "react" ;
9+ import { useEffect , useEffectEvent , useRef , useState } from "react" ;
1010
1111import { previewBridge } from "~/components/preview/previewBridge" ;
1212import { usePreviewBridge } from "~/components/preview/usePreviewBridge" ;
1313import { useClientSettingsHydrated } from "~/hooks/useSettings" ;
1414import { cn , isMacPlatform } from "~/lib/utils" ;
1515
16- import { resolveBrowserSurfacePanelRect , useBrowserSurfaceStore } from "./browserSurfaceStore" ;
16+ import {
17+ type BrowserSurfaceContentPresentation ,
18+ type BrowserSurfaceRect ,
19+ resolveBrowserSurfacePanelRect ,
20+ useBrowserSurfaceStore ,
21+ } from "./browserSurfaceStore" ;
1722import { useActiveBrowserRecordingTabIds } from "./browserRecording" ;
1823import {
1924 browserViewportSettingKey ,
@@ -136,15 +141,14 @@ export function HostedBrowserWebview(props: {
136141
137142 const [ webviewGeneration , setWebviewGeneration ] = useState ( 0 ) ;
138143 const [ recoverySrc , setRecoverySrc ] = useState ( initialSrc ) ;
139- const latestUrlRef = useRef ( initialUrl ) ;
140-
141- useEffect ( ( ) => {
142- latestUrlRef . current = initialUrl ;
143- } , [ initialUrl ] ) ;
144+ const remountCrashedWebview = useEffectEvent ( ( ) => {
145+ setRecoverySrc ( initialUrl ?? initialSrc ) ;
146+ setWebviewGeneration ( ( generation ) => generation + 1 ) ;
147+ } ) ;
144148
145- const setWebviewRef = useCallback ( ( node : HTMLElement | null ) => {
149+ const setWebviewRef = ( node : HTMLElement | null ) => {
146150 webviewRef . current = node as ElectronWebview | null ;
147- } , [ ] ) ;
151+ } ;
148152
149153 useEffect ( ( ) => {
150154 const webview = webviewRef . current ;
@@ -176,10 +180,7 @@ export function HostedBrowserWebview(props: {
176180 crashRecoveryRef . current = recovery . state ;
177181 recoveryTimeout = setTimeout ( ( ) => {
178182 recoveryTimeout = null ;
179- if ( ! disposed ) {
180- setRecoverySrc ( latestUrlRef . current ?? initialSrc ) ;
181- setWebviewGeneration ( ( generation ) => generation + 1 ) ;
182- }
183+ if ( ! disposed ) remountCrashedWebview ( ) ;
183184 } , recovery . delayMs ) ;
184185 } ;
185186 // A click inside the guest only reaches this document as a webview focus
@@ -203,7 +204,13 @@ export function HostedBrowserWebview(props: {
203204 webview . removeEventListener ( "render-process-gone" , recoverGuest ) ;
204205 webview . removeEventListener ( "focus" , dismissHostPopups ) ;
205206 } ;
206- } , [ clientSettingsHydrated , config , initialSrc , runtimeTabId , webviewGeneration ] ) ;
207+ } , [
208+ clientSettingsHydrated ,
209+ config ,
210+ runtimeTabId ,
211+ // oxlint-disable-next-line react/exhaustive-effect-dependencies -- A crash recovery remounts the webview under a new key, so the listeners attach to the new element.
212+ webviewGeneration ,
213+ ] ) ;
207214
208215 const active = presentation . visible && presentation . rect !== null ;
209216 const lastRect = presentation . rect ;
@@ -214,25 +221,10 @@ export function HostedBrowserWebview(props: {
214221 viewportWidth === null || viewportHeight === null ? null : viewportWidth / viewportHeight ;
215222 const lockedAspectRatio =
216223 aspectRatioLocked && viewportAspectRatio !== null ? viewportAspectRatio : null ;
217- const handleAspectRatioChange = useCallback ( ( aspectRatio : number | null ) => {
224+ const handleAspectRatioChange = ( aspectRatio : number | null ) => {
218225 setAspectRatioLocked ( aspectRatio !== null ) ;
219- } , [ ] ) ;
220- const hiddenContentSize = presentation . content
221- ? {
222- width : presentation . content . width / presentation . content . scale ,
223- height : presentation . content . height / presentation . content . scale ,
224- }
225- : null ;
226- const hiddenSize =
227- viewport . _tag !== "fill"
228- ? {
229- width : viewport . width * normalizedZoomFactor ,
230- height : viewport . height * normalizedZoomFactor ,
231- }
232- : {
233- width : hiddenContentSize ?. width ?? lastRect ?. width ?? 1280 ,
234- height : hiddenContentSize ?. height ?? lastRect ?. height ?? 800 ,
235- } ;
226+ } ;
227+ const hiddenSize = useHiddenSize ( viewport , normalizedZoomFactor , presentation . content , lastRect ) ;
236228 const containerSize = active && lastRect ? lastRect : hiddenSize ;
237229 const deviceToolbarVisible = active && viewport . _tag !== "fill" && ! presentation . fitSourceContent ;
238230 const {
@@ -263,7 +255,7 @@ export function HostedBrowserWebview(props: {
263255 ? resolveBrowserViewportLayout ( lastRect , fittedSourceViewport , normalizedZoomFactor )
264256 : viewportLayout ;
265257
266- const syncContentPresentation = useCallback ( ( ) => {
258+ const syncContentPresentation = ( ) => {
267259 const wrapper = wrapperRef . current ;
268260 if ( ! wrapper ) return ;
269261 useBrowserSurfaceStore . getState ( ) . presentContent ( runtimeTabId , {
@@ -275,7 +267,7 @@ export function HostedBrowserWebview(props: {
275267 scrollLeft : wrapper . scrollLeft ,
276268 scrollTop : wrapper . scrollTop ,
277269 } ) ;
278- } , [ layout , runtimeTabId ] ) ;
270+ } ;
279271
280272 useEffect ( ( ) => {
281273 const frameId = window . requestAnimationFrame ( syncContentPresentation ) ;
@@ -286,7 +278,13 @@ export function HostedBrowserWebview(props: {
286278 const wrapper = wrapperRef . current ;
287279 if ( ! wrapper ) return ;
288280 wrapper . scrollTo ( { left : 0 , top : 0 } ) ;
289- } , [ runtimeTabId , viewport . _tag , viewportHeight , viewportWidth ] ) ;
281+ } , [
282+ // oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new tab or viewport size starts the canvas scrolled to its origin.
283+ runtimeTabId ,
284+ viewport . _tag ,
285+ viewportHeight ,
286+ viewportWidth ,
287+ ] ) ;
290288
291289 if ( ! clientSettingsHydrated || ! config ) return null ;
292290
@@ -392,3 +390,20 @@ export function HostedBrowserWebview(props: {
392390 </ div >
393391 ) ;
394392}
393+
394+ // The size feeds useBrowserViewportResize, whose layout keys the content sync effect.
395+ function useHiddenSize (
396+ viewport : PreviewViewportSetting ,
397+ zoomFactor : number ,
398+ content : BrowserSurfaceContentPresentation | null ,
399+ lastRect : BrowserSurfaceRect | null ,
400+ ) {
401+ "use memo" ;
402+ if ( viewport . _tag !== "fill" ) {
403+ return { width : viewport . width * zoomFactor , height : viewport . height * zoomFactor } ;
404+ }
405+ return {
406+ width : content ? content . width / content . scale : ( lastRect ?. width ?? 1280 ) ,
407+ height : content ? content . height / content . scale : ( lastRect ?. height ?? 800 ) ,
408+ } ;
409+ }
0 commit comments