[][src]Module breadx::tutorials::color

Last time, we discussed events and how to react to them. Today, we'll talk about color management. It may seem like a trivial subject, but it carries a surprising amount of importance within X11.

If you've used frameworks like GTK+ or web browsers, color management might have seemed surprisingly easy. You just pass in your elements representing your red, green, and blue, and the color comes out. Unfortunately, when you get to a level as low as X, it's not always that simple.

In an ideal world, all monitors would use standard 24-bit color. Every computer a technician could come across would support R, G and B. In 2020, most of us live in this ideal world. However, those with outdated hardware may be forced to confront a cold reality: monitors that can only hold a limited amount of color. As in, your monitor holds 16 colors. Once you've used all 16, you're out.

As an X programmer, you're confronted with a dilemma. Do you take the easy way out, or trudge through on the hard way?

The Easy Way

"Bah!" says the programmer, rubbing his unshaven chin and pushing up his glasses. "Why should I support every last piece of outdated, obsolete hardware out there?" He cracks his knuckles. "I'm perfectly fine with taking the easy way out!"

You might have noticed that functions like Display::create_simple_window take u32's as color values. If you're taking the easy way, then For All Intents and Purposes, this represents a 24-bit color. The lower 8 bits represents blue, the next-lower 8 bits represents green, and the next-lower represents red. In fact, breadx provides the rgb function as a convenience to construct colors in this way.

This code would display a window with a red background.

use breadx::rgb;

let red_color = rgb(std::u8::MAX, 0, 0);
let window = conn.create_simple_window(
        conn.default_root(),
        0,
        0,
        640,
        400,
        0,
        conn.default_black_pixel(),
        // NEW: red_color instead of conn.default_white_pixel()
        red_color,
    )?;
}

As said before, this will probably work. This will probably produce consistent and reliable results with most X servers. This probably won't break any code. This probably won't create hard to debug problems for end users on older hardware. This probably won't cause said users to report bugs on consumers of the library you wrote, and it probably won't make said consumers pull their hair out as they try desperately to trace the problem to code that you wrote.

Note

In all seriousness, the rgb function has worked for every computer I've tested it on, including every Linux machine I could get my hands on and an OSX laptop. It's generally a working solution. In addition, it's faster than the "Hard Way" described below, since it's a basic computation instead of sending a request to the server. For the rest of the tutorial, I'll be using the "easy way", since it's generally less verbose than the hard way.

To add to that, I'm pretty sure that, as of the time of writing, Rust won't even run on any computer where rgb could be expected to fail.

The Hard Way

If you've decided to take The Hard Way, you need to be aware of a few extra aspects of X11.

The first of these aspects is the Colormap. It can be envisioned as an array containing a series of colors. The alloc_color function allows you to insert a color into this colormap, and returns the index of the color. The u32 passed as colors are no longer encoded 24-bit color triplets, but these indexes into the colormap.

Consider the following program, which is equivalent to the Easy Way code above.

let red_color = conn
    .default_colormap()
    .alloc_color_immediate(std::u16::MAX, 0, 0)?
    .pixel();
let window = conn.create_simple_window(
        conn.default_root(),
        0,
        0,
        640,
        400,
        0,
        conn.default_black_pixel(),
        red_color,
    )?;
}

Dissecting `Colormap::alloc_color`

You may notice two things about the alloc_color_immediate function straight away:

  • It takes u16's instead of u8's, like rgb does.
  • It returns an object of type ColorAllocation, which has a method named pixel that returns the actual color result.

In the first case, X11 actually supports using u16's to create colors instead of u8. TODO: actually figure out if X supports truecolor

In the second case, X11 colormaps will try to return a color already allocated in the colormap if it no longer has any room for color. In case you care about this, breadx returns the enum to tell you whether or not the color you put into the function is equal to the color you're gettting. In case you don't, the pixel function is an easy way to get the result out.

Note that this way is somewhat slower than the Easy Way above. It sends a request to the server which, no matter how you dice it, it always going to be slower than a few bit shifts and some addition. Consider which method best suits you as you program.

Next time, we'll talk about how to draw on windows.