@@ -132,8 +132,10 @@ const SAFARI_SCREEN_GESTURE_INITIAL_SPREAD = 0.28;
132132const SAFARI_SCREEN_GESTURE_MIN_SCALE = 0.25 ;
133133const SAFARI_SCREEN_GESTURE_MAX_SCALE = 4 ;
134134const REAL_MULTITOUCH_GESTURE_GRACE_MS = 120 ;
135- const NATIVE_SCROLL_DELTA_SCALE = 4 ;
136- const NATIVE_SCROLL_MAX_DELTA = 180 ;
135+ const WHEEL_TOUCH_SCROLL_DELTA_SCALE = 0.0012 ;
136+ const WHEEL_TOUCH_SCROLL_MAX_STEP = 0.18 ;
137+ const WHEEL_TOUCH_SCROLL_EDGE_INSET = 0.08 ;
138+ const WHEEL_TOUCH_SCROLL_IDLE_MS = 90 ;
137139const DEFAULT_ACCESSIBILITY_MAX_DEPTH = 10 ;
138140const LOGICAL_INSPECTOR_MAX_DEPTH = 80 ;
139141const FLUTTER_INSPECTOR_MAX_DEPTH = 48 ;
@@ -176,6 +178,11 @@ interface StreamQualityResponse {
176178 videoCodec ?: string ;
177179}
178180
181+ interface WheelTouchScrollState {
182+ udid : string ;
183+ current : Point ;
184+ }
185+
179186function buildChromeUrl (
180187 udid : string ,
181188 stamp : string ,
@@ -613,6 +620,8 @@ export function AppShell({
613620 const scrollAccumulatorRef = useRef < Point > ( { x : 0 , y : 0 } ) ;
614621 const scrollFrameRef = useRef < number > ( 0 ) ;
615622 const scrollPointRef = useRef < Point > ( { x : 0.5 , y : 0.5 } ) ;
623+ const wheelTouchScrollRef = useRef < WheelTouchScrollState | null > ( null ) ;
624+ const wheelTouchScrollEndTimeoutRef = useRef < number > ( 0 ) ;
616625 const effectiveZoomRef = useRef ( initialViewportState . zoom ?? 1 ) ;
617626 const panRef = useRef < Point > ( initialViewportState . pan ) ;
618627 const applyZoomAtClientPointRef = useRef <
@@ -661,6 +670,11 @@ export function AppShell({
661670 window . cancelAnimationFrame ( scrollFrameRef . current ) ;
662671 scrollFrameRef . current = 0 ;
663672 }
673+ if ( wheelTouchScrollEndTimeoutRef . current !== 0 ) {
674+ window . clearTimeout ( wheelTouchScrollEndTimeoutRef . current ) ;
675+ wheelTouchScrollEndTimeoutRef . current = 0 ;
676+ }
677+ endWheelTouchScroll ( "cancelled" ) ;
664678 } ;
665679 } , [ ] ) ;
666680
@@ -2613,7 +2627,7 @@ export function AppShell({
26132627 ) ;
26142628 setAccessibilitySelectedId ( "" ) ;
26152629 setAccessibilityHoveredId ( null ) ;
2616- sendScrollWheel ( deltaX , deltaY , screenPoint . x , screenPoint . y ) ;
2630+ sendWheelTouchScroll ( deltaX , deltaY , screenPoint . x , screenPoint . y ) ;
26172631 return true ;
26182632 }
26192633
@@ -3110,6 +3124,97 @@ export function AppShell({
31103124 }
31113125 }
31123126
3127+ function clampWheelTouchPoint ( point : Point ) : Point {
3128+ return {
3129+ x : clampNumber (
3130+ point . x ,
3131+ WHEEL_TOUCH_SCROLL_EDGE_INSET ,
3132+ 1 - WHEEL_TOUCH_SCROLL_EDGE_INSET ,
3133+ ) ,
3134+ y : clampNumber (
3135+ point . y ,
3136+ WHEEL_TOUCH_SCROLL_EDGE_INSET ,
3137+ 1 - WHEEL_TOUCH_SCROLL_EDGE_INSET ,
3138+ ) ,
3139+ } ;
3140+ }
3141+
3142+ function wheelTouchStartPoint ( anchor : Point , step : Point ) : Point {
3143+ const clampedAnchor = clampWheelTouchPoint ( anchor ) ;
3144+ const maxX = 1 - WHEEL_TOUCH_SCROLL_EDGE_INSET ;
3145+ const maxY = 1 - WHEEL_TOUCH_SCROLL_EDGE_INSET ;
3146+ let x = clampedAnchor . x ;
3147+ let y = clampedAnchor . y ;
3148+
3149+ if ( step . x > 0 ) {
3150+ x = Math . min ( x , maxX - Math . abs ( step . x ) ) ;
3151+ } else if ( step . x < 0 ) {
3152+ x = Math . max ( x , WHEEL_TOUCH_SCROLL_EDGE_INSET + Math . abs ( step . x ) ) ;
3153+ }
3154+
3155+ if ( step . y > 0 ) {
3156+ y = Math . min ( y , maxY - Math . abs ( step . y ) ) ;
3157+ } else if ( step . y < 0 ) {
3158+ y = Math . max ( y , WHEEL_TOUCH_SCROLL_EDGE_INSET + Math . abs ( step . y ) ) ;
3159+ }
3160+
3161+ return clampWheelTouchPoint ( { x, y } ) ;
3162+ }
3163+
3164+ function wheelDeltaToTouchStep ( deltaX : number , deltaY : number ) : Point {
3165+ return {
3166+ x : clampNumber (
3167+ - deltaX * WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
3168+ - WHEEL_TOUCH_SCROLL_MAX_STEP ,
3169+ WHEEL_TOUCH_SCROLL_MAX_STEP ,
3170+ ) ,
3171+ y : clampNumber (
3172+ - deltaY * WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
3173+ - WHEEL_TOUCH_SCROLL_MAX_STEP ,
3174+ WHEEL_TOUCH_SCROLL_MAX_STEP ,
3175+ ) ,
3176+ } ;
3177+ }
3178+
3179+ function endWheelTouchScroll ( phase : "ended" | "cancelled" = "ended" ) {
3180+ if ( wheelTouchScrollEndTimeoutRef . current !== 0 ) {
3181+ window . clearTimeout ( wheelTouchScrollEndTimeoutRef . current ) ;
3182+ wheelTouchScrollEndTimeoutRef . current = 0 ;
3183+ }
3184+
3185+ const active = wheelTouchScrollRef . current ;
3186+ wheelTouchScrollRef . current = null ;
3187+ if ( ! active ) {
3188+ return ;
3189+ }
3190+
3191+ sendControl ( active . udid , {
3192+ type : "touch" ,
3193+ ...active . current ,
3194+ phase,
3195+ } ) ;
3196+ }
3197+
3198+ function scheduleWheelTouchScrollEnd ( ) {
3199+ if ( wheelTouchScrollEndTimeoutRef . current !== 0 ) {
3200+ window . clearTimeout ( wheelTouchScrollEndTimeoutRef . current ) ;
3201+ }
3202+ wheelTouchScrollEndTimeoutRef . current = window . setTimeout ( ( ) => {
3203+ wheelTouchScrollEndTimeoutRef . current = 0 ;
3204+ endWheelTouchScroll ( "ended" ) ;
3205+ } , WHEEL_TOUCH_SCROLL_IDLE_MS ) ;
3206+ }
3207+
3208+ function beginWheelTouchScroll ( udid : string , anchor : Point , step : Point ) {
3209+ const start = wheelTouchStartPoint ( anchor , step ) ;
3210+ wheelTouchScrollRef . current = { udid, current : start } ;
3211+ sendControl ( udid , {
3212+ type : "touch" ,
3213+ ...start ,
3214+ phase : "began" ,
3215+ } ) ;
3216+ }
3217+
31133218 function flushScrollWheel ( ) {
31143219 scrollFrameRef . current = 0 ;
31153220 const accumulated = scrollAccumulatorRef . current ;
@@ -3120,13 +3225,13 @@ export function AppShell({
31203225
31213226 const deltaX = clampNumber (
31223227 accumulated . x ,
3123- - NATIVE_SCROLL_MAX_DELTA ,
3124- NATIVE_SCROLL_MAX_DELTA ,
3228+ - WHEEL_TOUCH_SCROLL_MAX_STEP / WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
3229+ WHEEL_TOUCH_SCROLL_MAX_STEP / WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
31253230 ) ;
31263231 const deltaY = clampNumber (
31273232 accumulated . y ,
3128- - NATIVE_SCROLL_MAX_DELTA ,
3129- NATIVE_SCROLL_MAX_DELTA ,
3233+ - WHEEL_TOUCH_SCROLL_MAX_STEP / WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
3234+ WHEEL_TOUCH_SCROLL_MAX_STEP / WHEEL_TOUCH_SCROLL_DELTA_SCALE ,
31303235 ) ;
31313236 const remainingX = accumulated . x - deltaX ;
31323237 const remainingY = accumulated . y - deltaY ;
@@ -3135,15 +3240,15 @@ export function AppShell({
31353240 scrollFrameRef . current = window . requestAnimationFrame ( flushScrollWheel ) ;
31363241 }
31373242
3138- sendScrollWheelNow (
3243+ sendWheelTouchScrollNow (
31393244 deltaX ,
31403245 deltaY ,
31413246 scrollPointRef . current . x ,
31423247 scrollPointRef . current . y ,
31433248 ) ;
31443249 }
31453250
3146- function sendScrollWheel (
3251+ function sendWheelTouchScroll (
31473252 deltaX : number ,
31483253 deltaY : number ,
31493254 x : number ,
@@ -3162,15 +3267,15 @@ export function AppShell({
31623267 y : Number . isFinite ( y ) ? clampNumber ( y , 0 , 1 ) : 0.5 ,
31633268 } ;
31643269 scrollAccumulatorRef . current = {
3165- x : scrollAccumulatorRef . current . x + deltaX * NATIVE_SCROLL_DELTA_SCALE ,
3166- y : scrollAccumulatorRef . current . y + deltaY * NATIVE_SCROLL_DELTA_SCALE ,
3270+ x : scrollAccumulatorRef . current . x + deltaX ,
3271+ y : scrollAccumulatorRef . current . y + deltaY ,
31673272 } ;
31683273 if ( scrollFrameRef . current === 0 ) {
31693274 scrollFrameRef . current = window . requestAnimationFrame ( flushScrollWheel ) ;
31703275 }
31713276 }
31723277
3173- function sendScrollWheelNow (
3278+ function sendWheelTouchScrollNow (
31743279 deltaX : number ,
31753280 deltaY : number ,
31763281 x : number ,
@@ -3186,17 +3291,61 @@ export function AppShell({
31863291 ) {
31873292 return ;
31883293 }
3294+
3295+ const udid = selectedSimulator . udid ;
3296+ const anchor = {
3297+ x : clampNumber ( x , 0 , 1 ) ,
3298+ y : clampNumber ( y , 0 , 1 ) ,
3299+ } ;
3300+ const step = wheelDeltaToTouchStep ( deltaX , deltaY ) ;
3301+ if ( step . x === 0 && step . y === 0 ) {
3302+ return ;
3303+ }
3304+
3305+ if ( wheelTouchScrollRef . current ?. udid !== udid ) {
3306+ endWheelTouchScroll ( "cancelled" ) ;
3307+ }
3308+
3309+ if ( ! wheelTouchScrollRef . current ) {
3310+ beginWheelTouchScroll ( udid , anchor , step ) ;
3311+ }
3312+
3313+ let active = wheelTouchScrollRef . current ;
3314+ if ( ! active ) {
3315+ return ;
3316+ }
3317+
3318+ let next = clampWheelTouchPoint ( {
3319+ x : active . current . x + step . x ,
3320+ y : active . current . y + step . y ,
3321+ } ) ;
31893322 if (
3190- ! sendControl ( selectedSimulator . udid , {
3191- type : "scroll" ,
3192- deltaX,
3193- deltaY,
3194- x : clampNumber ( x , 0 , 1 ) ,
3195- y : clampNumber ( y , 0 , 1 ) ,
3323+ Math . hypot ( next . x - active . current . x , next . y - active . current . y ) < 0.001
3324+ ) {
3325+ endWheelTouchScroll ( "ended" ) ;
3326+ beginWheelTouchScroll ( udid , anchor , step ) ;
3327+ active = wheelTouchScrollRef . current ;
3328+ if ( ! active ) {
3329+ return ;
3330+ }
3331+ next = clampWheelTouchPoint ( {
3332+ x : active . current . x + step . x ,
3333+ y : active . current . y + step . y ,
3334+ } ) ;
3335+ }
3336+
3337+ active . current = next ;
3338+ if (
3339+ ! sendControl ( udid , {
3340+ type : "touch" ,
3341+ ...next ,
3342+ phase : "moved" ,
31963343 } )
31973344 ) {
31983345 setLocalError ( "Simulator control stream disconnected." ) ;
3346+ return ;
31993347 }
3348+ scheduleWheelTouchScrollEnd ( ) ;
32003349 }
32013350
32023351 function prepareSimulatorInput ( ) {
0 commit comments