Our Own Image

Strangely enough, in a tutorial about user interface building, nothing visible will change in this chapter. And neither will it in the coming chapters. Instead, we will focus on what funcions gtk supplies that help us developing a more responsive interface. And what limitations it has. You can find the code of this stage in branch 6-vec-image.

Currently, when the mandelbrot picture is computed, the user interface freezes. Try the Flamenco preset, maximize the window and try to change something in the user interface and you will see what I mean. In the next stage, we will use a separate thread to compute the image. That means that we will have to send the image from one thread to another. Now, recall that we used an ImageSurface from cairo to represent the images. Unfortunately, if we at the Auto Trait Implementation section of its documentation, we see !Send. That means, we cannot send it between threads! This is not typical for that class, but is the rule for gtk classes. We read in the glib documentation:

Each class and interface has a corresponding smart pointer struct representing an instance of that type (e.g. Object for GObject or gtk::Widget for GtkWidget). They are reference counted and feature interior mutability similarly to Rust’s Rc<RefCell> idiom.

And just as Rc is not Send, so are the classes in glib, gtk and the other crates of the GNOME stack for rust.

In this stage we will develop our own image class, such that we will be able to send image data from one thread to another in the next stage.

The class ImageSurface that we use has another constructor function:

#![allow(unused)]
fn main() {
pub unsafe fn create_for_data_unsafe(
    data: *mut u8,
    format: Format,
    width: i32,
    height: i32,
    stride: i32
) -> Result<ImageSurface, Error>
}

The documentation says that it creates an image surface for the provided pixel data.

  • The pointer data is the beginning of the underlying slice, and at least width * stride succeeding bytes should be allocated.
  • data must live longer than any reference to the returned surface.
  • You have to free data by yourself.

Because the function is unsafe, there is every reason to follow these rules. So, let's try to do that. You can find this code in file image.rs.

#![allow(unused)]
fn main() {
pub struct Image {
    _data: Vec<u8>,
    surface: ImageSurface,
}

impl Image {
    pub fn new(mut data: Vec<u8>, format: Format, width: i32, height: i32, stride: i32) -> Image {
        assert!(data.len() >= height as usize * stride as usize);
        let surface;
        unsafe {
            surface = ImageSurface::create_for_data_unsafe(
                data.as_mut_ptr(),
                format,
                width,
                height,
                stride,
            )
            .unwrap();
        }
        Image {
            _data: data,
            surface,
        }
    }
    pub fn surface(&self) -> &ImageSurface {
        &self.surface
    }
}
}

We store the Vec<u8> with the data next to the ImageSurface. The only way to access the image surface is through the function surface, which gives a reference to it. The lifetime of such a reference can be no longer than the lifetime of the corresponding Image struct, so we seem to be good with rule 2. Rule 3 is no problem either, as the Vec will remove the data when the Image is removed. Rule 1 is kind of dealt with with the assertion. Kind of, because we use height times stride in the assertion instead of width times stride in the rule. That is, because I think the rule is wrong.

We use an underscore before the data field, because the rust compiler thinks that we don't use it and would emit a warning otherwise:

field data is never read

If we would run the program without the next addition, it would show the image, but at the first change there would be a segmentation error. Ouch! What's happening here? We're not programming in C, are we? And yes, we used unsafe code, but didn't we follow the rules so that we should be OK?

What bites us here is the very poor documentation of the cairo crate, I think. Actually, it took me days to find out what could be done about this error. Finally I stumbled upon the documentation of the corresponding C function cairo_image_surface_create_for_data, which says:

The output buffer must be kept around until the cairo_surface_t is destroyed or cairo_surface_finish() is called on the surface. So, let's add the following:

#![allow(unused)]
fn main() {
impl Drop for Image {
    fn drop(&mut self) {
        self.surface.finish();
    }
}
}

So, we call the finish function on the surface just before the Image is destroyed. That does the trick. And to prove my claim about the poor documentation of this function, I will reproduce it here:

Yes, that was it. As we say in Dutch: "Zero, nada, noppes". Hopefully this tutorial will get outdated soon in this respect.

Creating and Using the Image Data

We make are mandelbrot image in the function make_mandel_image. We want to return data that we could move between threads. The class Image that we just defined is not a candidate, because it contains an ImageSurface which is not Send. But a Vec<u8>, which we can use to construct an Image, can be moved. So we return an optional tuple with the Vec<u8> with the image data as first part and the stride as second part.

#![allow(unused)]
fn main() {
pub fn make_mandel_image(
    mapping: &Mapping,
    col_producer: &Box<dyn Coloring>,
) -> Option<(Vec<u8>, i32)> {
    if !mapping.is_valid() {
        return None;
    }
    match IMG_FMT.stride_for_width(mapping.win_width as u32) {
        Err(_) => None,
        Ok(stride) => {
            let h = mapping.win_height as usize;
            let ustride = stride as usize;
            let mut surface: Vec<u8> = vec![0; h * ustride];
            fill_mandel_image(surface.as_mut(), ustride, mapping, col_producer);
            Some((surface, stride))
        }
    }
}
}

We find the stride with the stride_for_width function which is defined for the cairo Format class. We create a vec of the appropriate size and call fill_mandel image to fill it. That function took a mutable slice already.

In file gui.rs we use the image. We adapt the state and make the img field have the type Option<Image>. In mandel_draw we use img.surface() as first parameter of set_source_surface.

The biggest change is in

#![allow(unused)]
fn main() {
fn recompute_image(state: &mut State) {
    let coloring = state.color_info.scheme(state.col_idx);
    if let Some((img_vec, stride)) = make_mandel_image(&state.mapping, coloring) {
        let img = Image::new(
            img_vec,
            IMG_FMT,
            state.mapping.win_width as i32,
            state.mapping.win_height as i32,
            stride,
        );
        state.img = Some(img);
        if let Some(canvas) = state.canvas.upgrade() {
            canvas.queue_draw();
        }
    }
}
}

The call of make_mandel_image returns a Vec which we use to create an Image.
We put that image in the state.

That was it. I encourage you to play with the code now. Uncomment the finish call and see it crash. Or move the finish call inside the new function and see what happens. After that, we are ready for really using the freshly developed class in the next chapter.