The gtk-rs way

Sometimes, it’s not possible yet to do something in relm, for instance creating a menu or drawing a widget. This is because some features of GTK+ behaves differently than others and the assumptions made in relm does not support these features. However, it’s always possible to mix the gtk-rs and the relm ways and that’s what we’ll see in this tutorial.

Creating a menu

If we try to create a menu like we would create other widgets in relm, it won’t work. For instance, this code:

gtk::MenuBar {
    gtk::MenuItem {
        label: "File",
        gtk::Menu {
            gtk::MenuItem {
                label: "Quit",
            },
        },
    },
}

will not show the Quit menu and GTK+ will give this warning:

Attempting to add a widget with type GtkMenu to a container of type GtkMenuItem, but the widget is already inside a container of type GtkWindow, please remove the widget from its existing container first.

So instead of doing this, we’ll write some gtk-rs code and integrate it into our relm widget. First, let’s create our view with the MenuBar:

view! {
    gtk::Window {
        gtk::Box {
            orientation: Vertical,
            #[name="menubar"]
            gtk::MenuBar {
            },
        },
        delete_event(_, _) => (Quit, Inhibit(false)),
    }
}

As you can see here, we give a name to the menu bar which will allow us to access to it with self.menubar.

Then, we’ll implement the init_view() method which is called right after the view is created:

#[widget]
impl Widget for Win {
    fn init_view(&mut self) {
        let file_menu = gtk::Menu::new();
        let file_item = gtk::MenuItem::new_with_label("File");
        file_item.set_submenu(Some(&file_menu));
        let quit_item = gtk::MenuItem::new_with_label("Quit");
        self.menubar.append(&file_item);
        file_menu.append(&quit_item);
        self.menubar.show_all();

        connect!(quit_item, connect_activate(_), self.model.relm, Quit);
    }
}

Here, we have normal gtk-rs code to create the menu and append it to our menu bar that was created the relm way. And we connect the event with the connect! macro like we saw earlier.

We also access the relm object from the model, which we saved in the model() method:

    fn model(relm: &Relm<Self>, _: ()) -> Model {
        Model {
            relm: relm.clone(),
        }
    }

As you can see, the model() method can have multiple signatures (actually, the #[widget] attribute will add these two parameters if they are not provided). This one takes the Relm object as its first parameter. This object is used to connect events to send a message to a relm widget or to directly send a message to a relm widget.

The complete code for this section is here.

Drawing example

We’ll write another application that draw the letter pressed by the user.

Our model will contain the letter pressed by the user:

pub struct Model {
    letter: Rc<Cell<char>>,
}

Here, we use a Rc<Cell<_>> as we would do in gtk-rs because we want to share it with an event callback. When relm will support drawing natively, that should not be required anymore.

The initial value of the model will be:

fn model() -> Model {
    Model {
        letter: Rc::new(Cell::new(' ')),
    }
}

Our view will contain a drawing area:

view! {
    gtk::Window {
        #[name="drawing_area"]
        gtk::DrawingArea {
        },
        delete_event(_, _) => (Quit, Inhibit(false)),
        key_press_event(_, event) => (KeyPress(event.clone()), Inhibit(false)),
    }
}

And we connected the key press event to get the key pressed by the user.

We handle this message as such:

fn update(&mut self, event: Msg) {
    match event {
        KeyPress(event) => {
            if let Some(letter) = gdk::keyval_to_unicode(event.get_keyval()) {
                self.model.letter.set(letter);
                self.drawing_area.queue_draw();
            }
        },
        Quit => gtk::main_quit(),
    }
}

which converts the key to a Unicode character and assign it to our model while requesting to redraw the widget.

Finally, in the init_view() method, we do the actual drawing:

fn init_view(&mut self) {
    let letter = self.model.letter.clone();
    self.drawing_area.connect_draw(move |_, context| {
        context.set_source_rgb(0.2, 0.4, 0.0);
        context.paint();

        context.set_font_size(60.0);
        context.set_source_rgb(0.0, 0.0, 0.0);
        context.move_to(100.0, 100.0);
        context.show_text(&letter.get().to_string());
        Inhibit(false)
    });
}

We clone the reference-counted letter to have a copy of it in the draw event handler. In this handler, we draw the letter using cairo.

The complete code for this section is here.