High quality scrolling with Emacs

2023-03-05 11:30:30 CET

Doom Emacs convinced me to switch to emacs after being a long time vim user. Naturally, I spent a lot of time tweaking my emacs setup 😃 and I settled with Emacs 29.0 using native-compilation (see also gcc-emacs).

On macOS, I use a custom built emacs-plus formula. The specific command line is:

$ brew install emacs-plus@29 --with-xwidgets --with-native-comp

Native comp speeds things up and Xwidgets adds a built-in web browser based on webkit. As for the version, 29 feels much snappier than 28. This is more pronounced on macOS, 28 was OK on Linux. No idea why.

High-Quality Scrolling

One of the addition of Emacs 29 is the pixel-scroll-precision-mode. Just enable it and, if you are using a windowed version of emacs, you should have a vertical scroll that is pixel-based rather than line-based.

This feels much better when using trackpad scrolling:

Fixing wheel-based horizontal scrolling and text scaling

By default, Emacs regroups multiple scroll events into a single one large enough to scroll one line. This produces much fewer events with a coarse precision. This goes against the smooth experience sought by pixel-scroll-precision-mode, so it disables this feature by setting mwheel-coalesce-scroll-events to nil.

Unfortunately, this affects all wheel events, while pixel-scroll-precision-mode cares only about vertical scrolling. Other wheel-based features go crazy (for instance, scaling text with a mouse wheel is roughly 20 times faster on my setup, quite inconvenient).

My tentative fix is to switch coalescing on and off based on the action.

For this I defined two helper functions:

(defun filter-mwheel-always-coalesce (orig &rest args)
  "A filter function suitable for :around advices that ensures only 
   coalesced scroll events reach the advised function."
  (if mwheel-coalesce-scroll-events
      (apply orig args)
    (setq mwheel-coalesce-scroll-events t)))

(defun filter-mwheel-never-coalesce (orig &rest args)
  "A filter function suitable for :around advices that ensures only 
   non-coalesced scroll events reach the advised function."
  (if mwheel-coalesce-scroll-events
      (setq mwheel-coalesce-scroll-events nil)
    (apply orig args)))

Before forwarding a scroll event, they check whether mwheel-coalesce-scroll-events matches the expectation, and either forward the event or change the configuration.

When switching the event is dropped, which seems questionable but is actually preferable in my experience.

Finally, we can advise the wheel sensitive functions accordingly:

; Don't coalesce for high precision scrolling
(advice-add 'pixel-scroll-precision :around #'filter-mwheel-never-coalesce)

; Coalesce for default scrolling (which is still used for horizontal scrolling)
; and text scaling (bound to ctrl + mouse wheel by default).
(advice-add 'mwheel-scroll          :around #'filter-mwheel-always-coalesce)
(advice-add 'mouse-wheel-text-scale :around #'filter-mwheel-always-coalesce)

Horizontal scrolling?

By default horizontal scrolling is not enabled in Emacs.

To change that:

(setq mouse-wheel-tilt-scroll t)

If you like reversed / natural scrolling, also set:

(setq mouse-wheel-flip-direction t)