import { VerticalRefreshRateContext } from '@seorii/win32-displayconfig' ; import { BrowserWindow } from './browserWindow' ; import * as electron from 'electron' ; import process from 'process' ; import debug from './debug' ; function sleep ( duration ) { return new Promise < void > ( ( resolve ) => setTimeout ( resolve , duration ) ) ; } function areBoundsEqual ( left , right ) { return ( left . height === right . height && left . width === right . width && left . x === right . x && left . y === right . y ) ; } const billion = 1000 * 1000 * 1000 ; function hrtimeDeltaForFrequency ( freq ) { return BigInt ( Math . ceil ( billion / freq ) ) ; } let disableJitterFix = false ; function isInSnapZone ( ) { const point = electron . screen . getCursorScreenPoint ( ) ; const display = electron . screen . getDisplayNearestPoint ( point ) ; return ( ( point . x > display . bounds . x - 20 && point . x < display . bounds . x + 20 ) || ( point . x > display . bounds . x + display . bounds . width - 20 && point . x < display . bounds . x + display . bounds . width + 20 ) ) ; } /**
 * Unfortunately, we have to re-implement moving and resizing.
 * Enabling vibrancy slows down the window's event handling loop to the
 * point building a mouse event backlog. If you just handle these events
 * in the backlog without taking the time difference into consideration,
 * you end up with visible movement lag.
 * We tried pairing 'will-move' with 'move', but Electron actually sends the
 * 'move' events _before_ Windows actually commits to the operation. There's
 * likely some queuing going on that's getting backed up. This is not the case
 * with 'will-resize' and 'resize', which need to use the default behavior
 * for compatibility with soft DPI scaling.
 * The ideal rate of moving and resizing is based on the vertical sync
 * rate: if your display is only fully updating at 120 Hz, we shouldn't
 * be attempting to reset positions or sizes any faster than 120 Hz.
 * If we were doing this in a browser context, we would just use
 * requestAnimationFrame and call it a day. But we're not inside of a
 * browser context here, so we have to resort to clever hacks.
 * This VerticalRefreshRateContext maps a point in screen space to the
 * vertical sync rate of the display(s) actually handing that point.
 * It handles multiple displays with varying vertical sync rates,
 * and changes to the display configuration while this process is running.
 */ export default function win10refresh ( win , maximumRefreshRate ) { const refreshCtx = new VerticalRefreshRateContext ( ) ; function getRefreshRateAtCursor ( cursor ) { cursor = cursor || electron . screen . getCursorScreenPoint ( ) ; return refreshCtx . findVerticalRefreshRateForDisplayPoint ( cursor . x , cursor . y ) ; } let pollingRate ; let doFollowUpQuery = false , isMoving = false , shouldMove = false ; let moveLastUpdate = BigInt ( 0 ) , resizeLastUpdate = BigInt ( 0 ) ; let lastWillMoveBounds , lastWillResizeBounds , desiredMoveBounds ; let boundsPromise = Promise . race ( [ getRefreshRateAtCursor ( electron . screen . getCursorScreenPoint ( ) ) . then ( ( rate ) => { pollingRate = rate || 30 ; doFollowUpQuery = true ; } ) , sleep ( 2000 ) . then ( ( ) => { pollingRate = pollingRate || 30 ; } ) , ] ) ; async function doFollowUpQueryIfNecessary ( cursor ) { if ( doFollowUpQuery ) { const rate = await getRefreshRateAtCursor ( cursor ) ; if ( rate != pollingRate ) debug ( `New polling rate: ${ rate }` ) ; pollingRate = rate || 30 ; } } function setWindowBounds ( bounds ) { if ( win . isDestroyed ( ) ) return ; win . setBounds ( bounds ) ; desiredMoveBounds = win . getBounds ( ) ; } function currentTimeBeforeNextActivityWindow ( lastTime , forceFreq ? ) { return ( process . hrtime . bigint ( ) < lastTime + hrtimeDeltaForFrequency ( forceFreq || pollingRate || 30 ) ) ; } function guardingAgainstMoveUpdate ( fn ) { if ( pollingRate === undefined || ! currentTimeBeforeNextActivityWindow ( moveLastUpdate ) ) { moveLastUpdate = process . hrtime . bigint ( ) ; fn ( ) ; return true ; } else { return false ; } } win . on ( 'will-move' , ( e , newBounds ) => { if ( win . __electron_acrylic_window__ . opacityInterval ) return ; if ( lastWillMoveBounds !== undefined && areBoundsEqual ( lastWillMoveBounds , newBounds ) ) { e . preventDefault ( ) ; return ; } if ( lastWillMoveBounds ) { newBounds . width = lastWillMoveBounds . width ; newBounds . height = lastWillMoveBounds . height ; } lastWillMoveBounds = newBounds ; const didOptimisticMove = ! isMoving && guardingAgainstMoveUpdate ( ( ) => { desiredMoveBounds = undefined ; } ) ; if ( didOptimisticMove ) { boundsPromise = boundsPromise . then ( doFollowUpQueryIfNecessary ) ; return ; } e . preventDefault ( ) ; if ( win . __electron_acrylic_window__ . moveTimeout ) clearTimeout ( win . __electron_acrylic_window__ . moveTimeout ) ; win . __electron_acrylic_window__ . moveTimeout = setTimeout ( ( ) => { shouldMove = false ; } , 1000 / Math . min ( pollingRate , maximumRefreshRate ) ) ; disableJitterFix = isInSnapZone ( ) ; if ( ! shouldMove ) { shouldMove = true ; if ( isMoving ) return false ; isMoving = true ; const basisBounds = win . getBounds ( ) ; const basisCursor = electron . screen . getCursorScreenPoint ( ) ; function handleIntervalTick ( moveInterval ) { boundsPromise = boundsPromise . then ( ( ) => { if ( ! shouldMove ) { isMoving = false ; clearInterval ( moveInterval ) ; return ; } const cursor = electron . screen . getCursorScreenPoint ( ) ; const didIt = guardingAgainstMoveUpdate ( ( ) => { if ( lastWillResizeBounds && lastWillResizeBounds . width ) setWindowBounds ( { x : Math . floor ( basisBounds . x + ( cursor . x - basisCursor . x ) ) , y : Math . floor ( basisBounds . y + ( cursor . y - basisCursor . y ) ) , width : Math . floor ( lastWillResizeBounds . width / electron . screen . getDisplayMatching ( basisBounds ) . scaleFactor ) , height : Math . floor ( lastWillResizeBounds . height / electron . screen . getDisplayMatching ( basisBounds ) . scaleFactor ) , } ) ; else setWindowBounds ( { x : Math . floor ( basisBounds . x + ( cursor . x - basisCursor . x ) ) , y : Math . floor ( basisBounds . y + ( cursor . y - basisCursor . y ) ) , width : Math . floor ( lastWillMoveBounds . width / electron . screen . getDisplayMatching ( basisBounds ) . scaleFactor ) , height : Math . floor ( lastWillMoveBounds . height / electron . screen . getDisplayMatching ( basisBounds ) . scaleFactor ) , } ) ; } ) ; if ( didIt ) { return doFollowUpQueryIfNecessary ( cursor ) ; } } ) ; } const moveInterval = setInterval ( ( ) => handleIntervalTick ( moveInterval ) , 1000 / 600 ) ; } } ) ; win . on ( 'move' , ( e ) => { if ( disableJitterFix ) { return false ; } if ( isMoving || win . isDestroyed ( ) ) { e . preventDefault ( ) ; return false ; } if ( desiredMoveBounds !== undefined ) { const forceBounds = desiredMoveBounds ; desiredMoveBounds = undefined ; win . setBounds ( { x : Math . floor ( forceBounds . x ) , y : Math . floor ( forceBounds . y ) , width : Math . floor ( forceBounds . width ) , height : Math . floor ( forceBounds . height ) , } ) ; } } ) ; win . on ( 'will-resize' , ( e , newBounds ) => { if ( lastWillResizeBounds !== undefined && areBoundsEqual ( lastWillResizeBounds , newBounds ) ) { e . preventDefault ( ) ; return ; } lastWillResizeBounds = newBounds ; if ( pollingRate !== undefined && currentTimeBeforeNextActivityWindow ( resizeLastUpdate , Math . min ( pollingRate , maximumRefreshRate ) ) ) { e . preventDefault ( ) ; return false ; } resizeLastUpdate = process . hrtime . bigint ( ) ; } ) ; win . on ( 'resize' , ( ) => { resizeLastUpdate = process . hrtime . bigint ( ) ; boundsPromise = boundsPromise . then ( doFollowUpQueryIfNecessary ) ; } ) ; win . on ( 'closed' , refreshCtx . close ) ; }