One Size Fits All?

When we resize the application window of the base stage, we see that the picture will not fill the window anymore. This is what we will fix in this stage. The code of this stage can be found on branch 1-resize. window with badly resized mandelbrot image

The program now needs to react to changes and so will need to maintain some state. We define this struct in a submodule of gui. In this state we keep the size of the drawing area and the picture that corresponds to that size. In fact, we will keep a Mapping object instead of just the size of the drawing area. This will be handy in future stages. And the fields in the state correspond to the argument and result of the function make_mandel_image, which is quite convenient.

#![allow(unused)]
fn main() {
struct State {
    mapping: Mapping,
    img: Option<ImageSurface>,
}

impl State {
    pub fn new() -> State {
        State {
            mapping: Mapping::new_for_size(WIN_SZ0),
            img: None,
        }
    }
    pub fn img(&self) -> &Option<ImageSurface> {
        &self.img
    }
    pub fn on_resize(&mut self, w: i32, h: i32) {
        self.mapping.win_width = w as usize;
        self.mapping.win_height = h as usize;
        self.img = make_mandel_image(&self.mapping);
    }
}
}

The function new creates a mapping where the mandelbrot image just fits in the original drawing area. The image is empty.

The function img returns the current image. It is used in the drawing function.

The function on_resize is called when the size of the window changes. It stores the size in the mapping. The it calls make_mandel_image and stores the result in the img field.

We initialize the state at the start of the build_ui function:

#![allow(unused)]
fn main() {
let state = Rc::new(RefCell::new(State::new()));
}

Why do we need to wrap the state in a reference counted RefCell? The state will be accessed by several functions that we register with gtk. In this stage we need it for drawing and for reacting to the resize signal. In the latter case, we change the state. So we use the interior mutability pattern as described in the rust book. Gtk guarantees that all user interface calls occur on the same thread, so we don't need a mutex or atomic reference counting.

When we build the user interface, we need to inform the application that we want to react to a change of size to the drawing area. There is a signal (resize) that we can connect to.

#![allow(unused)]
fn main() {
canvas.connect_resize(move |_da, w, h| state.borrow_mut().on_resize(w, h));
}

That function should take the drawing area and the new width and height as parameter. We don't need the first, but we do need the state, so we pass it a closure that captures the state. The closure borrows the state mutably and calls on_resize on it.

The other thing that needs to change is how we draw in the drawing area. We have to draw the image that is in the state, instead of the static image. So, in build_ui we have:

#![allow(unused)]
fn main() {
    let state2 = state.clone();
    canvas.set_draw_func(move |_d, ctxt, _w, _h| mandel_draw(&state2, ctxt));
}

Note that we clone the state and move the cloned value. We still need the state in connect_resize that appears after it.

#![allow(unused)]
fn main() {
fn mandel_draw(state: &Rc<RefCell<State>>, ctxt: &gtk::cairo::Context) {
    if let Some(img) = &state.borrow().img() {
        ctxt.set_source_surface(img, 0.0, 0.0)
            .expect("Expected to be able to set source surface");
        ctxt.paint().unwrap();
    }
}
}

The function mandel_draw needs a small adaptation. We don't pass the static image, but the state and get the image from the state. Now the picture will again fill the whole drawing area: window with nicely resized mandelbrot image