Vaxry's blog

01000100010011

What the Cursor?

10 III 2024

48.4k

As I've recently launched hyprcursor, a completely new cursor format for use in a modern desktop aimed at replacing the old standard of XCursor, I think it's a good idea to make a blogpost about how cursors actually work on Linux (and BSD, blah blah) in general to help people understand what's going on.

Terminology

A quick one on the terms used:

How does a cursor work?

"Well lol just draw a png haha"

Well, no, it's not that simple.

Rasters are annoying. Raster means an image is stored with pixels, for example a png, or an mp4. The problem with rasters is that when you resize them, the computer has to take a wild guess at what it should look like. There are various algorithms that do it, but the most used are bilinear and nearest neighbor.

Nearest neighbor will produce a "pixelated" look, while bilinear will make it more blurry.

Now, if we just encoded our cursor in e.g. 128x128, we could just downscale it to the user-requested size and be done with it, right?

Well, not exactly. No matter the algorithm, it will almost never produce pixel-perfect results. It will almost always look "odd".

That's why, with raster cursors, the best course of action is to just encode a few common sizes and be done with it. That's what XCursor does.

Every XCursor shape is just a few images at different sizes. 96, 64, 48, 32, 24, 18, etc.

The obvious problem with this is that first, we are storing multiple images per shape, and second, that we can't get anything in-between.

XCursor does not allow for resizing those images, so either the client has to do it or it's not happening. In most cases, it's not happening.

That's not too bad

Oh wait, we got more!

Most cursor sizes have their largest images at 96x96. That's, on an 8K screen, already considerably smaller than a 32px on a 1080p screen.

You might think that a 1080p PNG is like a megabyte, so why not encode more? like 128px and 256px?

Well, the reason is simple. XCursor predates PNG. GIF too. JPEG as well. That's why, XCursor images are... uncompressed.

A max 96x96 Bibata cursor, a single cursor, is about 2MB. That's a lot. The entire theme is 44.1MB. Ouch.

Amendment: A single static cursor in Bibata is almost 170kB, and animated ones are about 16MB each, my bad. The 44.1MB figure is correct.

A modern format

To stop storing uncompressed pixel data like a caveman, I've made hyprcursor.

Hyprcursor has a few key advantages:

Let's elaborate on each point a bit:

Storage format

Your cursors can be either PNG, which is an efficient, extremely widely used, lossless raster image format, or SVG, which is the most adopted standard for vector-based images.

Almost all cursors start out as an SVG, because they are infinitely scalable. An SVG basically describes the "shape" of your cursor instead of the raw pixels, and is then rendered at different resolutions for packing into a theme.

With hyprcursor, hyprcursor itself can do the rendering, which has two benefits:

Metadata

Some cursors cannot become SVGs, and that's fine. Some cursors are used for more than one shape, and that's fine. Some cursors are animated, and that's fine!

All of a shape's properties are described in a small meta.hl file alongside them.

For overrides, instead of being a caveman and making symlinks all over the place, a simple define_override = name in the meta is enough.

For raster shapes, you can define what scaling algorithm to use, if any, so that you can control how your cursors scale to sizes outside of the ones you've made.

API

Rendering a cursor shape with hyprcursor to a png on disk takes about 10 lines of C if we exclude error checking. With it, it's about 25.

You can also use the C++ API, which does some additional QoL stuff behind the scenes for you :)

Space efficiency

PNGs are way more efficient than uncompressed data, bringing down Bibata from 44.1MB to about 6MB. However, with svg, we get down to 171kB, which is a 250x reduction.

Okay okay but how is that used?

Who draws the cursor? Well, you might assume it's the, well, display server, the compositor, right?

Well, kind of.

In the beginning, the earth was without form, and void. Wait, a bit too far behind.

In the beginning of wayland, and in the core design spec, it's the client's job to draw the cursor. I mean, it kind of makes sense, as some apps might want to draw a custom cursor (e.g. games)

However, for desktop usage, that kinda sucks. Each app has to support rendering cursors, and some apps might render them differently, leading to inconsistencies. You probably have encountered them especially if using a HiDPI screen.

Recently, we've got the wp_cursor_shape protocol, which allows the apps to just say "hey compositor, render a shape", which is miles better, as the compositor can now draw the cursor.

There are about 35 defined shapes in the protocol, which is, to be honest, enough.

If your compositor supports hyprcursor (like hyprland), then whenever server-side cursors are being used, hyprcursor will also be utilized. However, if client-side, the classic style, is used, it's up to the app, and most, at the moment, don't support hyprcursors.

What's the support looking like?

Well, right now, not perfect. QT, chromium, electron and hypr* apps support cursor_shape, so in hyprland (or any other compositor using hyprcursors) you will get your pixel-perfect hyprcursors.

Notably, GTK does not support the protocol, so apps like firefox, or the entire gnome suite will fall back to the ugly XCursor themes.

Will this ever be adopted?

I don't know! All I know is that it's a clearly superior system that is easy to implement.

The best path, in my opinion, would be to push for wp_cursor_shape adoption over at gtk. If gtk finally supports the protocol, almost all desktop apps that people might wanna use on Linux will be able to utilize hyprcursors via the compositor.

Support from the toolkits themselves for hyprcursor itself is likely not going to happen, as hyprcursor is not made by a million-dollar non-profit entity via a 10-year-long bureaucratic process, unless somehow the developers over there decide it's more beneficial to use hyprcursors now rather than wait and develop their own, new standard. Who knows? :)

How do I make a hyprcursor theme? Can I use my current themes?

You can convert an xcursor theme to a hyprcursor theme in 5 minutes using the hyprcursor-util in the hyprcursor repo.

However, you probably would rather go to the original cursor source and replace all the generated .png files with the source .svg versions to get the benefits of vector images. This process will take a bit longer, but is surely worth it :) Remember to update the meta.hl with the new images!

If you want to share the theme with other people (and it is allowed via its license), feel free to post it in #hyprcursor-themes on the Hyprland discord server :)

Check out the docs and the util on the hyprcursor github page for more. You can also find instructions there for the end users and developers wishing to adopt this library in the docs/ directory.

Closing thoughts

Thanks to everyone for the constant support towards making the Hypr* project better and better!

We've got some big plans moving forward so stick around for the journey :)

As a small gift, here's bibata's left_ptr, rendered with hyprcursor at 4K, just because I thought it was funny:

click here

I tried 16K but it almost crashed my computer lol


Questions, comments, mistakes? Ping me a mail at vaxry [at] vaxry.net and I'll get back to ya.