ultra-scroll
1 is a smooth-scrolling package for emacs, with native
support for standard builds as well as
emacs-mac. It provides
highly optimized, pixel-precise smooth scrolling which can readily keep
up with the very high event rates of modern track-pads and
high-precision wheel mice.
You move your fingers, the page responds, instantly:
usm.mov
Importantly, ultra-scroll
can cleanly scroll right across tall
images and other jumbo lines – a perennial problem with scrolling
packages to date. As a bonus, it enables relatively smooth scrolling
even with dumb third party mice on some systems.
Note, the previous-buffer
animation above is from two-finger track-pad
swiping, and is an emacs-mac
exclusive.
Note
Do you need this?
If you don't scroll with a high-speed device (modern mouse or track-pad), no. If you do, but aren't sure, here's a good test to try:
Open a heavy emacs buffer full screen on your largest monitor. While scrolling smoothly such that lines would move across your window's full height in about 5 seconds, can you easily read the text you see, without stopping, in both directions? Now, try this exercise again with your browser – I bet it's very readable there. Shouldn't emacs be like this?
If you scroll buffers with tall images visible, this is also a good
reason to give ultra-scroll
a try.
See the NEWS.
ultra-scroll
should work across all systems that provide pixel
scrolling information for your hardware; see the PIXEL-DELTA
field of
wheel-up/down
events.
Note
If your mouse and/or system do not provide real pixel scrolling data,
ultra-scroll
will be equivalent to line-by-line scrolling. To check,
run M-x ultra-scroll-check
, and follow the direction. If it reports
spurious pixel scroll data, consider using
pixel-scroll-precision-mode
with
pixel-scroll-precision-interpolate-mice
instead.
Important
Your Help Needed! While ultra-scroll
works out of the box for
most people, it's impossible to test all combinations of systems and
hardware, so please take a moment to report your smooth-scrolling
experiences.
Not yet in a package archive. For Emacs 29.1 and later, use
package-vc-install
. In the *scratch*
buffer, enter
(package-vc-install '(ultra-scroll :vc-backend Git :url "https://github.com/jdtsmith/ultra-scroll"))
move to the final paren, and C-x C-e
. Installation is then simple:
(use-package ultra-scroll
;:load-path "~/code/emacs/ultra-scroll" ; if you git clone'd instead of package-vc-install
:init
(setq scroll-conservatively 101 ; important!
scroll-margin 0)
:config
(ultra-scroll-mode 1))
Just start scrolling :).
Tip
For best performance, use a build with native-compilation (see Speed).
If it doesn't seem to do anything for you, please run
M-x ultra-scroll-check
. If this command reports the pixel scroll data
are all the same, ultra-scroll
won't do much for you. Look into
getting hardware/system support for PIXEL-DELTA
scrolling event
information, or switch to pixel-scroll-precision-mode
, which provides
simulated smooth scrolling via interpolation.
There is little to no configuration.
If desired for use with dumb mice on emacs-mac
, the variable
ultra-scroll-mac-multiplier
can be set to a number smaller or larger
than 1.0
to decrease/increase mouse-wheel scrolling speed. Note that
many fancier wheeled mice have drivers that simulate track-pads, so
this variable will have no effect on them. For these, and for track-pads
generally, scrolling speed should be configured in system settings.
Note
Only certain systems provide real variable pixel scroll offset data
(PIXEL-DELTA
) for older/wheeled ("dumb") mice. Use
M-x ultra-scroll-check
to see if yours does. If not, it's
recommended to upgrade hardware, or stick with
pixel-scroll-precision-mode
.
To reduce the likelihood of garbage collection during scroll, which can
introduce slight pauses, the value of gc-cons-percentage
is
temporarily increased, and reset during idle time. The defaults should
work well for most situations, but if necessary, can be configured using
ultra-scroll-gc-percentage
and ultra-scroll-gc-idle-time
.
By design, ultra-scroll
uses the in-built
pixel-scroll-precision-mode-map
, remapping its scrolling function with
its own. The latter also has the capability of faking smooth scrolling
using interpolation for non mouse movements, e.g. for
scroll-up/down-command
. Simply set the relevant variables, like
pixel-scroll-precision-interpolate-page
, and they should "just work".
Important
Unlike pixel-scroll-precision-mode-map
, ultra-scroll
does not
support "faking" smooth pixel-level scrolling for mouse wheel movement
on systems that do not provide accurate data. You can determine if
yours does with M-x ultra-scroll-check
. If not, it's recommended to
stick with pixel-scroll-precision-mode
.
emacs-mac's own builtin mac-mwheel-scroll
This venerable code was introduced with
emacs-mac more than a
decade ago, and was the first to provide smooth scrolling in emacs.
pixel-scroll-precision-mode
A fast pixel scrolling by Po Lu, built in to Emacs as of v29.1 (see
pixel-scroll.el
). Does not support emacs-mac
. ultra-scroll
was
initially based on its design, but many design elements have changed.
pixel-scroll-mode
A simpler line-by-line pixel scrolling mode, also found in the file
pixel-scroll.el
.
good-scroll
An update to pixel-scroll-mode
with variable speed.
sublimity
Includes smooth scrolling based on sublime editor.
Picture it: a fast new laptop and 5K monitor with a large heavy-duty,
full-screen buffer in python-ts-mode
. Scrolling line-by-line with a
decent mouse is mostly OK, but smooth pixel scrolling with the track-pad
is just… painful. Repeated attempts to rationalize this fail,
especially because it's notably worse in one direction than the other.
Scrolling Emacs feels like moving through (light) molasses. No bueno.
Checking into it, the smooth scroll event callback takes 15-20ms scrolling in one direction, and 3–5x longer in the other. This performance is perfectly fine for normal mice which deliver a few scrolling events a second. But track-pad scroll events are arriving every 15ms or less! The code just couldn't keep up. Hence the molasses.
I also wanted to be able to scroll through image-rich documents without worrying about jumpy/loopy scrolling behavior. And my extra dumb mouse didn't work well either: small scrolls did nothing: you'd have scroll pretty aggressively to get any movement at all.
How hard could it be to fix this? And the adventure began…
This packaged used to be called ultra-scroll-mac
. The emacs-mac
port
of emacs exposes pixel-level scrolling event stream of Mac track-pads
(and other fancy mice) in a distinct way, which is not supported by
pixel-scroll-precision-mode
. And unfortunately the default
smooth-scrolling library included in emacs-mac
is quite low
performance (see above).
On the emacs-mac
build, there is no comparison, because
pixel-scroll-precision-mode
doesn't work there. On other builds, they
are fairly comparable. Compared to pixel-scroll-precision-mode
,
ultra-scroll
obviously works with emacs-mac
, but is also even
faster, and can cleanly scroll past images taller than the
window.
In addition to fast scrolling, the built-in
pixel-scroll-precision-mode
(new in Emacs v29.1) can simulate a
feature-complete track-pad driver in elisp for older mice which do not
supply pixel scroll information. This comes complete with elisp-based
scroll interpolation, a timer-based momentum phase, etc.
Emacs was designed long before mice were common, not to mention modern high-resolution track-pads which send rapid micro-updates ("move up one pixel!") more than 60 times per second. Unlike other programs, Emacs insists on keeping the cursor (point) visible at all times. Deep in its re-display code, Emacs tracks where point is, and works diligently to ensure it never falls outside the visible window. It does this not by moving point (that's the user's job), but by moving the window (visible range of lines) surrounding point.
Once you are used to this behavior, it's actually pretty nice for
navigating with C-n
/ C-p
and friends. But for smooth scrolling with
a track-pad or mouse, it is very problematic – nothing screams "janky
scrolling" like the window lurching back or forth half a page during a
scroll. Or worse: getting caught in an endless loop of
scroll-in-one-direction/jump-back-in-the-other.
So what should be done? The elisp info manual (Textual Scrolling
/
set-window-start
) helpfully mentions:
…for reliable results Lisp programs that call this function should always move point to be inside the window whose display starts at POSITION.
Which is all well and good, but where do you find such a point, in advance, safely inside the window? Often this isn't terribly hard, but there is one common case where this admonition falls comically flat: scrolling past an image or other content which is taller than the window – what I call jumbo lines. Where can I place point inside the window when a jumbo line occupies the entire window height?
As a result of these types of difficulties, pixel scrolling codes and packages are often quite involved, with much of the logic boiling down to a stalwart and increasingly heroic pile of interwoven attempts to keep the damn point on screen and prevent juddering and looping as you scroll.
For posterity, some things I discovered in my own mostly-victorious battle against unwanted re-centering during smooth scroll, including across jumbo lines:
scroll-conservatively=101
is very helpful, since with this Emacs will "scroll just enough text to bring point into view, even if you move far away". It does not defeat re-centering, but makes it… more manageable.- You cannot let-bind
scroll-conservatively
for effect, as it comes into play only on re-display (after your event handler returns). scroll-margin>0
is a no-no. This setting always moves point at least that many lines from the window boundaries, which, unless you can reliably place point there during the scroll (even in the presence of jumbo lines; see below), will cause loop-back. See #3.- Virtual Scroll:
vscroll
– a virtual rendered scrolling window hiding below the current window – is key to smooth scrolling, and alteringvscroll
to move the view-port is incredibly fast.- There is plenty of
vscroll
room available, including the entirety of any tall lines (as for displayed images) in view. vscroll
can sometimes place the point off the visible window (I know, sacrilege), but more often triggers re-centering.
- Scrolling asymmetry:
- Sadly
vscroll
is purely one-sided: you can only access avscroll
area beneath the current window view; there is no negativevscroll
. - Unlike
window-start
,window-end
does not get updated promptly between re-displays and cannot always be trusted. Computing it is expensive, so should be avoided during re-display. - For these two reasons, smooth scrolling up and scrolling down are not symmetric with each other, and will likely never be. You need different approaches for each.
- If the two approaches for scrolling up and down perform quite differently, the user will definitely feel this difference.
- Sadly
- For avoiding re-centering, naive movement doesn't work well. You need to learn the basic layout of lines on the window before re-display has occurred.
- The "usable window height" deducts any header and the old-fashioned tab-bar, but not the tab-bar-mode bar.
- Jumbo lines (lines taller than the window's height):
- Scrolling towards buffer end:
- When scrolling with jumbo lines towards the buffer's end (with
vscroll
), simply keep point on the jumbo line until it fully disappears from view. As a special case, Emacs will not re-center when this happens. - This is not true for lines that are shorter than the usable window height. In this case, you must avoid placing point on any line which falls partially out of view.
- When scrolling with jumbo lines towards the buffer's end (with
- Scrolling towards buffer start:
- When scrolling up past jumbo lines towards the buffer's start
using
set-window-start
(lines of content move down), you must keep point on the jumbo, but only until it clears the top of the window area (even by one pixel). - After this, you must move the point to the line above it (and had
better insist that
scroll-conservatively>0
to prevent re-centering). - In some cases (depending on truncation/visual-line-mode/etc.), this movement must occur from a position beyond the first full height object (which may not be at the line's start). E.g. one before the visual line end.
- When scrolling up past jumbo lines towards the buffer's start
using
- Scrolling towards buffer end:
pos-visible-in-window
doesn't always give correct results near the window boundaries. Better to use the first line at the window's top or directly identify the final line (both viapos-at-x-y
) and adjust from there.- Display bugs
- There are display bugs with inline images that cause them to misreport pixel measurements and positions sometimes.
- These lead to slightly staccato scrolling in such buffers and
height=0
gets erroneously reported, so can't be used to find beginning of buffer. Best to guard against these. - Update: Two display bugs have been fixed in master as of Dec, 2023, so scrolling with lots of inline images will soon be even smoother. One bug related to motion skipping visual-wrapped lines with images at line start remains.
So all in all, it's quite complicated to get something that works as
you'd hope. The cutting room floor is littered with literally dozens of
almost-but-not-quite-working versions of ultra-scroll
. I'm sure there
are many more corner cases, but the current design gets most things
right in my usage.
I often wonder how many people who claim "emacs is laggy" form that impression from scrolling. Scrolling at 60Hz or faster with modern mice and track-pads puts a lot of stress on systems, and is often the first place lag appears.
ultra-scroll
is fast by design. I made some observations about its
speed using ELP
to measure the average call duration of individual
scroll functions (ultra-scroll-up/down
) with various buffer and window
sizes2.
- Very large window sizes and buffers with "extra" processing going on, like treesitter, LSP modes, elaborate font-locking, tons of overlays, etc. can slow down scrolling.
- If the scroll command does its work in <10ms, you do not notice it. You can definitely start feeling it when scroll commands take more than 15ms.
- The underlying scroll primitives need to leave substantial overhead in time, so that all the other emacs commands that occur when new content is brought into view (font-lock) can run without causing scroll lag, for all your different modes. Faster is better: 3ms or less would be ideal.
- Building
--with-native-comp
is essential for ultra-smooth scrolling. It increases the speed of each individual scroll commands by >3x, which is important since these commands are called so frequently. - On the same build (NS, v29.4, with native-comp),
ultra-scroll
is about 40% faster thanpixel-scroll-precision-mode
. Except on slower machines, or in very heavy buffers and/or on large window sizes where your performance is right on the edge, this shouldn't be too noticeable. - On the same system (an M2 mac),
ultra-scroll
onemacs-mac
is 10-15% faster than on NS builds likeemacs-plus
. Very likely not noticeable. - The mode-line gets updated very often during smooth scrolls (and
in general), and poorly written fancy modeline add-ons are a common
source of slow-down. Good modeline modes will rate-limit their
updates behind timers and/or cache results in local/global
variables. If your scrolling (or any other aspect of Emacs) "lags",
try
(setq mode-line-format "NADA")
and see if that solves it. If so, suspect your fancy modeline.
Footnotes
-
Formerly
ultra-scroll-mac
. ↩ -
To try this yourself,
M-x elp-instrument-function
on bothultra-scroll-up/down
, scroll around (both directions) in a big buffer with a large window, thenM-x elp-results
. The last column gives average time in seconds. Less than 0.003s (i.e. 3ms) is ideal, 8ms is still perfectly usable, 15ms you'll feel a bit, 50ms will be very frustrating.scroll-down
is always faster thanscroll-up
due to an asymmetry in Emacs'vscroll
buffer. ↩