@@ -41,6 +41,10 @@ class nsZenWorkspaces {
4141 _workspaceCache = [ ] ;
4242
4343 #lastScrollTime = 0 ;
44+ #lastHorizontalWheelEventTime = 0 ;
45+ #horizontalScrollAccumulator = 0 ;
46+ #horizontalScrollFinalizeTimer = null ;
47+ #horizontalWheelGestureActive = false ;
4448
4549 bookmarkMenus = [
4650 "PlacesToolbar" ,
@@ -526,17 +530,25 @@ class nsZenWorkspaces {
526530 if ( ! this . workspaceEnabled || ! gNavToolbox . matches ( ":hover" ) ) {
527531 return ;
528532 }
533+ // Some devices emit AppCommand and wheel for the same horizontal wheel action.
534+ // Ignore AppCommand if a horizontal wheel event just occurred.
535+ if ( Date . now ( ) - this . #lastHorizontalWheelEventTime < 250 ) {
536+ return ;
537+ }
538+ if ( this . #horizontalWheelGestureActive) {
539+ this . #cancelHorizontalWheelGesture( ) ;
540+ }
529541
530542 const direction = this . naturalScroll ? - 1 : 1 ;
531543 // event is forward or back
532544 switch ( event . command ) {
533545 case "Forward" :
534- this . changeWorkspaceShortcut ( 1 * direction ) ;
546+ this . changeWorkspaceShortcut ( 1 * direction , true ) ;
535547 event . stopImmediatePropagation ( ) ;
536548 event . preventDefault ( ) ;
537549 break ;
538550 case "Back" :
539- this . changeWorkspaceShortcut ( - 1 * direction ) ;
551+ this . changeWorkspaceShortcut ( - 1 * direction , true ) ;
540552 event . stopImmediatePropagation ( ) ;
541553 event . preventDefault ( ) ;
542554 break ;
@@ -551,8 +563,9 @@ class nsZenWorkspaces {
551563 #setupSidebarHandlers( ) {
552564 const toolbox = gNavToolbox ;
553565
554- const scrollCooldown = 200 ; // Milliseconds to wait before allowing another scroll
555- const scrollThreshold = 1 ; // Minimum scroll delta to trigger workspace change
566+ const verticalScrollCooldown = 200 ; // Milliseconds to wait before allowing another scroll
567+ const verticalScrollThreshold = 1 ;
568+ const horizontalSnapPositionThreshold = 55 ;
556569
557570 toolbox . addEventListener (
558571 "wheel" ,
@@ -561,12 +574,20 @@ class nsZenWorkspaces {
561574 return ;
562575 }
563576
564- // Only process non-gesture scrolls
565- if ( event . deltaMode !== 1 ) {
577+ const absDeltaX = Math . abs ( event . deltaX ) ;
578+ const absDeltaY = Math . abs ( event . deltaY ) ;
579+ const isHorizontalScroll = absDeltaX > 0 && absDeltaX >= absDeltaY ;
580+ const isVerticalScroll = absDeltaY > 0 && absDeltaY > absDeltaX ;
581+
582+ if ( ! isHorizontalScroll && ! isVerticalScroll ) {
566583 return ;
567584 }
568585
569- const isVerticalScroll = event . deltaY && ! event . deltaX ;
586+ // Horizontal mouse wheels can report pixel deltas, so only enforce
587+ // line-based deltas for vertical scrolling.
588+ if ( ! isHorizontalScroll && event . deltaMode !== 1 ) {
589+ return ;
590+ }
570591
571592 //if the scroll is vertical this checks that a modifier key is used before proceeding
572593 if ( isVerticalScroll ) {
@@ -585,20 +606,61 @@ class nsZenWorkspaces {
585606 }
586607 }
587608
588- let currentTime = Date . now ( ) ;
589- if ( currentTime - this . #lastScrollTime < scrollCooldown ) {
609+ const currentTime = Date . now ( ) ;
610+
611+ if ( isHorizontalScroll ) {
612+ if ( this . #inChangingWorkspace) {
613+ return ;
614+ }
615+ this . #startHorizontalWheelGesture( ) ;
616+ const scrollDirection = this . naturalScroll ? - 1 : 1 ;
617+ const deltaPixels = this . #normalizeHorizontalWheelDelta( event ) * scrollDirection ;
618+ if ( ! deltaPixels ) {
619+ return ;
620+ }
621+ this . #lastHorizontalWheelEventTime = currentTime ;
622+ if (
623+ this . #horizontalScrollAccumulator !== 0 &&
624+ Math . sign ( this . #horizontalScrollAccumulator) !== Math . sign ( deltaPixels )
625+ ) {
626+ this . #horizontalScrollAccumulator = 0 ;
627+ }
628+ this . #horizontalScrollAccumulator += deltaPixels ;
629+ const stripWidth =
630+ window . windowUtils . getBoundsWithoutFlushing (
631+ document . getElementById ( "navigator-toolbox" )
632+ ) . width +
633+ window . windowUtils . getBoundsWithoutFlushing (
634+ document . getElementById ( "zen-sidebar-splitter" )
635+ ) . width *
636+ 2 ;
637+ let translateX = this . #horizontalScrollAccumulator;
638+ let forceMultiplier = Math . max ( 0.5 , 1 - Math . abs ( translateX ) / ( stripWidth * 4.5 ) ) ;
639+ translateX *= forceMultiplier ;
640+ if ( Math . abs ( deltaPixels ) > 0.8 ) {
641+ this . _swipeState . direction = deltaPixels > 0 ? "left" : "right" ;
642+ }
643+ const currentWorkspace = this . getActiveWorkspaceFromCache ( ) ;
644+ this . _organizeWorkspaceStripLocations ( currentWorkspace , true , translateX ) ;
645+ if ( Math . abs ( translateX ) >= horizontalSnapPositionThreshold ) {
646+ void this . #finalizeHorizontalWheelGesture( true ) ;
647+ return ;
648+ }
649+ this . #scheduleHorizontalWheelGestureFinalize( ) ;
590650 return ;
591651 }
592652
593- //this decides which delta to use
594- const delta = isVerticalScroll ? event . deltaY : event . deltaX ;
595- if ( Math . abs ( delta ) < scrollThreshold ) {
653+ if ( this . #horizontalWheelGestureActive) {
654+ this . #cancelHorizontalWheelGesture( ) ;
655+ }
656+ if ( currentTime - this . #lastScrollTime < verticalScrollCooldown ) {
657+ return ;
658+ }
659+ if ( Math . abs ( event . deltaY ) < verticalScrollThreshold ) {
596660 return ;
597661 }
598662
599- // Determine scroll direction
600- let rawDirection = delta > 0 ? 1 : - 1 ;
601-
663+ const rawDirection = event . deltaY > 0 ? 1 : - 1 ;
602664 let direction = this . naturalScroll ? - 1 : 1 ;
603665 this . changeWorkspaceShortcut ( rawDirection * direction ) ;
604666
@@ -608,6 +670,90 @@ class nsZenWorkspaces {
608670 ) ;
609671 }
610672
673+ #startHorizontalWheelGesture( ) {
674+ if ( this . #horizontalWheelGestureActive) {
675+ return ;
676+ }
677+ this . #horizontalWheelGestureActive = true ;
678+ gZenFolders . cancelPopupTimer ( ) ;
679+ document . documentElement . setAttribute ( "swipe-gesture" , "true" ) ;
680+ document . addEventListener ( "popupshown" , this . popupOpenHandler , { once : true } ) ;
681+ this . _swipeState = {
682+ isGestureActive : true ,
683+ lastDelta : 0 ,
684+ direction : null ,
685+ } ;
686+ Services . prefs . setBoolPref ( "zen.swipe.is-fast-swipe" , true ) ;
687+ }
688+
689+ #normalizeHorizontalWheelDelta( event ) {
690+ switch ( event . deltaMode ) {
691+ case event . DOM_DELTA_LINE :
692+ return event . deltaX * 40 ;
693+ case event . DOM_DELTA_PAGE :
694+ return event . deltaX * 160 ;
695+ case event . DOM_DELTA_PIXEL :
696+ default :
697+ return event . deltaX * 0.35 ;
698+ }
699+ }
700+
701+ #scheduleHorizontalWheelGestureFinalize( ) {
702+ if ( this . #horizontalScrollFinalizeTimer) {
703+ clearTimeout ( this . #horizontalScrollFinalizeTimer) ;
704+ }
705+ this . #horizontalScrollFinalizeTimer = setTimeout ( ( ) => {
706+ this . #horizontalScrollFinalizeTimer = null ;
707+ void this . #finalizeHorizontalWheelGesture( ) ;
708+ } , 180 ) ;
709+ }
710+
711+ async #finalizeHorizontalWheelGesture( forceSwitch = false ) {
712+ if ( ! this . #horizontalWheelGestureActive) {
713+ return ;
714+ }
715+ const threshold = 55 ;
716+ const shouldSwitch = forceSwitch || Math . abs ( this . #horizontalScrollAccumulator) >= threshold ;
717+ const workspaceOffset = this . #horizontalScrollAccumulator > 0 ? - 1 : 1 ;
718+ this . #horizontalScrollAccumulator = 0 ;
719+ if ( shouldSwitch ) {
720+ await this . changeWorkspaceShortcut ( workspaceOffset , true ) ;
721+ this . #lastScrollTime = Date . now ( ) ;
722+ } else {
723+ this . _cancelSwipeAnimation ( ) ;
724+ }
725+ this . #cleanupHorizontalWheelGesture( ) ;
726+ }
727+
728+ #cancelHorizontalWheelGesture( ) {
729+ if ( ! this . #horizontalWheelGestureActive) {
730+ return ;
731+ }
732+ if ( this . #horizontalScrollFinalizeTimer) {
733+ clearTimeout ( this . #horizontalScrollFinalizeTimer) ;
734+ this . #horizontalScrollFinalizeTimer = null ;
735+ }
736+ this . #horizontalScrollAccumulator = 0 ;
737+ this . _cancelSwipeAnimation ( ) ;
738+ this . #cleanupHorizontalWheelGesture( ) ;
739+ }
740+
741+ #cleanupHorizontalWheelGesture( ) {
742+ this . #horizontalWheelGestureActive = false ;
743+ this . _swipeState = {
744+ isGestureActive : false ,
745+ lastDelta : 0 ,
746+ direction : null ,
747+ } ;
748+ Services . prefs . setBoolPref ( "zen.swipe.is-fast-swipe" , false ) ;
749+ document . documentElement . removeAttribute ( "swipe-gesture" ) ;
750+ gZenUIManager . tabsWrapper . style . removeProperty ( "scrollbar-width" ) ;
751+ document . documentElement . style . setProperty ( "--zen-background-opacity" , "1" ) ;
752+ delete this . _hasAnimatedBackgrounds ;
753+ this . updateTabsContainers ( ) ;
754+ document . removeEventListener ( "popupshown" , this . popupOpenHandler , { once : true } ) ;
755+ }
756+
611757 initializeGestureHandlers ( ) {
612758 const elements = [
613759 gNavToolbox ,
@@ -651,6 +797,12 @@ class nsZenWorkspaces {
651797 _popupOpenHandler ( ) {
652798 // If a popup is opened, we should stop the swipe gesture
653799 if ( this . _swipeState ?. isGestureActive ) {
800+ if ( this . #horizontalScrollFinalizeTimer) {
801+ clearTimeout ( this . #horizontalScrollFinalizeTimer) ;
802+ this . #horizontalScrollFinalizeTimer = null ;
803+ }
804+ this . #horizontalWheelGestureActive = false ;
805+ this . #horizontalScrollAccumulator = 0 ;
654806 document . documentElement . removeAttribute ( "swipe-gesture" ) ;
655807 gZenUIManager . tabsWrapper . style . removeProperty ( "scrollbar-width" ) ;
656808 this . updateTabsContainers ( ) ;
0 commit comments