Making GTK keyboard on Rust

antonov_mike

Antonov Mike

Posted on July 17, 2022

Making GTK keyboard on Rust

FOREWORD

So I’m kinda newbie. I’ve never been studying programming before. And as self educated newbie I had to choose some direction as one of the best ways to study: learn, practice, repeat. Because of my experience of develop telegram bot (using carapax) and failure at making buttons I’ve started to learn GUI. I tried couple of other GUI libraries for Rust: druid, fltk and iced. And finally I opted for GTK because it’s docs looks a little bit better then previous ones. But I am not a qualified developer so maybe I am wrong.
First issue I encountered was the difficulty of reading the documentation and source code. It takes some getting used to.
The second – examples and explanations the people post in the internet. Sometimes there are just missed some parts of code. Some answers you won’t be able to guess your own. And you have to spend time for googling answers to questions you can’t even articulate because you have no experience at developing at all.
That’s why I made this post – to explain what I have learned to myself. It helps better understand what I have studied.

INTRO

Fist of all make a new Rust project:



cargo new gtk_grid


Enter fullscreen mode Exit fullscreen mode

After that we need all the dependencies to be added in our Cargo.toml file



[dependencies]
gtk = "0.15.5"
glib = "0.15.12"
chrono = "0.4.19"


Enter fullscreen mode Exit fullscreen mode

All the work would be done in an additional file gui.rs so let’s create it and add it to the main.rs with this line:



mod gui; 


Enter fullscreen mode Exit fullscreen mode

Main file main.rs would be like this. We won’t change it anymore. Just create GTK application, connect it with gui module and run it.



use gtk::prelude::*;

mod gui;

fn main() {
    let application = gtk::Application::new(
        Some("com.github.gtk-rs.examples.grid-packing"),
        Default::default(),
    );

    application.connect_activate(gui::build_ui);
    application.run();
}


Enter fullscreen mode Exit fullscreen mode

All the libraries we have to add to gui.rs file



use gtk::Label;
use glib::clone;
use gtk::prelude::*;
use gtk::Entry;
use chrono::Local;


Enter fullscreen mode Exit fullscreen mode

Great, now let’s get to work!

1. Window and Entry field

For this little example we only need two libraries:



use gtk::prelude::*;
use gtk::Entry;


Enter fullscreen mode Exit fullscreen mode

If we need to use function from gui.rs in main.rs we have to mark it as pub. This build_ui() function creates all the GTK elements, binds them together and displays in GTK window. Method named set_title(“App Name”) is a window description, adds your application’s name at the top of the window.
Other methods you can use:
window.set_titlebar(&header.container);
– An other window description. Makes a huge title bar. Lets you connect buttons right to the title bar.
window.set_wmclass("app-name", "App Name");
– Sets window manager class. Still not sure how it works.
Window::set_default_icon_name("icon-name");
– Sets default window’s icon. I tried and failed – no icon at all. Gonna check it later.
Now we are creating grid. Margin is just a digit. You can use different values for different margins and put them right into braces instead of variable. For example row_spacing(2). Connect the grid to the window as a child. We can make another child and connect it to. But as compiler says: “GtkApplicationWindow can only contain one widget at a time”, so we can’t add one more grid this way or even gtk::Button::with_label("Some Button"). I hope there is another way to connect more widgets.
After grid was connected we can create it’s elements and attach them to grid.
Let’s make an entry field using Entry::builder. I’ve never tried to add any functions to it, so it still just a demonstration model. I guess I solve this later.



pub fn build_ui(applicsation: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);
    window.set_title("GTK grid");
    window.set_default_size(200, 120);

    let margin = 6;
    let grid = gtk::Grid::builder()
        .margin_start(margin)
        .margin_end(margin)
        .margin_top(margin)
        .margin_bottom(margin)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .row_spacing(margin)
        .column_spacing(margin)
        .build();

    window.set_child(Some(&grid));

    let entry = Entry::builder()
        .margin_start(margin)
        .margin_top(margin)
        .margin_end(margin)
        .margin_bottom(margin)
        .build();
    grid.attach(&entry, 0, 0, 4 ,1);

    window.show_all();
}


Enter fullscreen mode Exit fullscreen mode

!!! Here is the thing about window.show() and window.show_all(): I saw a few examples of window.show() that only displays empty window and doesn’t show any widgets. Changing this line to window.show_all() helps. But I have no idea why some code works fine using show() and some needs show_all(). If you know please explain it to me.
After you compile and run your application should look like this (colors depend on your operation system settings)

Screenshot 01

2. First buttons

After we made an entry we are going to create a keyboard. We have to create each keyboard button with label, connect function to it and attach buttons to grid. Here you can see dummy functions like print string to console. Or you can make something more complicated. An example of the running button



    let button_1 = gtk::Button::with_label("Button 1"); 
    button_1.connect_clicked(glib::clone!(@weak grid => move |button| {
        let left_attach = grid.cell_left_attach(button);
        let new_left_attach = if left_attach == 2 { 0 } else { left_attach + 1 };
        grid.set_cell_left_attach(button, new_left_attach);
    }));
    grid.attach(&button_1, 0, 1, 1, 1);


Enter fullscreen mode Exit fullscreen mode

Don’t forget to remove button_2 and button_3 first. You can try it on your own so let’s get rid of it and use code that would make your app looks like on the next screenshot



    // --> KEYBOARD STARTS HERE <--

    let button_1 = gtk::Button::with_label("Button 1");
    let button_2 = gtk::Button::with_label("Button 2");
    let button_3 = gtk::Button::with_label("Button 3");
    button_1.connect_clicked(move |_| println!("Button 1"));
    button_2.connect_clicked(move |_| println!("Button 2"));
    button_3.connect_clicked(move |_| println!("Button 3"));
    grid.attach(&button_1, 0, 1, 1, 1);
    grid.attach(&button_2, 1, 1, 1, 1);
    grid.attach(&button_3, 2, 1, 1, 1);

    // --> ROW 2
    let button_4 = gtk::Button::with_label("Button 4");
    let button_5 = gtk::Button::with_label("Button 5");
    let button_6 = gtk::Button::with_label("Button 6");
    button_4.connect_clicked(move |_| println!("Button 4"));
    button_5.connect_clicked(move |_| println!("Button 5"));
    button_6.connect_clicked(move |_| println!("Button 6"));
    grid.attach(&button_4, 0, 2, 1, 1);
    grid.attach(&button_5, 1, 2, 1, 1);
    grid.attach(&button_6, 2, 2, 1, 1);

    // --> KEYBOARD ENDS HERE <--


Enter fullscreen mode Exit fullscreen mode

Grid attach takes 5 parameters: 1) the element you are attaching, 2) it’s horizontal position, 3) it’s vertical position, 4) it’s width, 5) and height.
These buttons are not scalable.

Screenshot 02

3. Other numeric buttons

Finish previous code with this part. Now we have a numeric phone-like keyboard full of useless buttons. Isn’t that wonderful?! Joking! I am gonna show you button with very important function in the next chapter. Just add this piece of code and check the screenshot №3.



    // --> KEYBOARD STARTS HERE <--

    // --> ROW 1
    let button_1 = gtk::Button::with_label("Button 1");
    let button_2 = gtk::Button::with_label("Button 2");
    let button_3 = gtk::Button::with_label("Button 3");
    button_1.connect_clicked(move |_| println!("Button 1"));
    button_2.connect_clicked(move |_| println!("Button 2"));
    button_3.connect_clicked(move |_| println!("Button 3"));
    grid.attach(&button_1, 0, 1, 1, 1);
    grid.attach(&button_2, 1, 1, 1, 1);
    grid.attach(&button_3, 2, 1, 1, 1);

    // --> ROW 2
    let button_4 = gtk::Button::with_label("Button 4");
    let button_5 = gtk::Button::with_label("Button 5");
    let button_6 = gtk::Button::with_label("Button 6");
    button_4.connect_clicked(move |_| println!("Button 4"));
    button_5.connect_clicked(move |_| println!("Button 5"));
    button_6.connect_clicked(move |_| println!("Button 6"));
    grid.attach(&button_4, 0, 2, 1, 1);
    grid.attach(&button_5, 1, 2, 1, 1);
    grid.attach(&button_6, 2, 2, 1, 1);

    // --> ROW 3
    let button_7 = gtk::Button::with_label("Button 7");
    let button_8 = gtk::Button::with_label("Button 8");
    let button_9 = gtk::Button::with_label("Button 9");
    button_7.connect_clicked(move |_| println!("Button 7"));
    button_8.connect_clicked(move |_| println!("Button 8"));
    button_9.connect_clicked(move |_| println!("Button 9"));
    grid.attach(&button_7, 0, 3, 1, 1);
    grid.attach(&button_8, 1, 3, 1, 1);
    grid.attach(&button_9, 2, 3, 1, 1);

    // --> ROW 4
    let button_0 = gtk::Button::with_label("Button 0");
    button_0.connect_clicked(move |_| println!("Button 0"));
    grid.attach(&button_0, 1, 4, 1, 1);

    // --> KEYBOARD ENDS HERE <--


Enter fullscreen mode Exit fullscreen mode

That how it gonna look like

Screenshot 03

4. Quit button

First of all add this to your gui.rs file



use glib::clone;


Enter fullscreen mode Exit fullscreen mode

Now it looks like that:



use gtk::prelude::*;
use gtk::Entry;
use glib::clone;


Enter fullscreen mode Exit fullscreen mode

Add the following code after row 4. Here we are making Quit Button and connecting it to function that gonna destroy our window, I mean close it. This part of code with window.destroy() is unsafe and I used it for the first time in my life. Such a historic moment!
Don’t forget to attach it to our grid: it would be the 4th element from the left and 2nd from the top with width equals 1 and height equals 4. Feel free to change everything



    // --> ROW 2 COLUMN 4 Quit button
    let quit_button = gtk::Button::with_label("Quit");
    quit_button.connect_clicked(clone!(@weak window => move |_| 
        unsafe {
            window.destroy()
        }
    ));
    grid.attach(&quit_button, 3, 1, 1, 4);

    // --> KEYBOARD ENDS HERE <--

    window.show_all();
}


Enter fullscreen mode Exit fullscreen mode

Let’s check the appearance

Screenshot 04

5. Label that contains a data

Add this to gui.rs file



use gtk::Label;


Enter fullscreen mode Exit fullscreen mode

That was an issue! But I did it and gonna tell you how.
Label is just a text. For example: let some_label = Label::new(Some("make code not war")); But we can connect function that gonna change this text. Let’s make label and two buttons for increase and decrease floating point integer.
Important: I still have no idea how to collect characters and keep them in the label. I can only replace them, so I can’t see nothing but 1 new character at a time. Maybe this is because the text() method consumes the value since it assembles all the chunks together, constructs a String is constructed, and returns it to you. We can't use our variable after calling text(). Don’t know. But month ago I had no idea how to make a GTK keyboard, so I guess I’m gonna solve this riddle to.
We are creating counter_label and attaching it to grid. That’s easy now.
The function (closure) need to clone some data. And we have to specify if this a weak or a strong clone. I’m not sure how it works I’m just following LSP and compiler’s hints, so we need a @weak clone here. Inside the closure we are making a variable by calling text() method and parsing it into floating point digit. If it succeed we can do some mathematics using set_text() method and format!() macro. The first one sets new text to our counter label. The second – calculates and turning the result into a String.
Now just attach all the parts to the appropriate places.



    // --> ROW 5 - LABEL
    let counter_label = Label::new(Some("0.0"));
    grid.attach(&counter_label, 0, 5, 4, 1);

    // --> ROW 4
    let plus_button = gtk::Button::with_label("+");
    let button_0 = gtk::Button::with_label("Button 0");
    let minus_button = gtk::Button::with_label("-");
    plus_button.connect_clicked(glib::clone!(@weak counter_label => move |_| {
        let nb = counter_label.text()
            .parse()
            .unwrap_or(0.0);
        counter_label.set_text(&format!("{}", nb + 1.1));
    }));
    button_0.connect_clicked(move |_| println!("Button 0"));
    minus_button.connect_clicked(glib::clone!(@weak counter_label => move |_| {
        let nb = counter_label.text()
            .parse()
            .unwrap_or(0.0);
        counter_label.set_text(&format!("{}", nb - 1.2));
    }));
    grid.attach(&plus_button, 0, 4, 1, 1);
    grid.attach(&button_0, 1, 4, 1, 1);
    grid.attach(&minus_button, 2, 4, 1, 1);


Enter fullscreen mode Exit fullscreen mode

It should look like this. Take a look at the bottom of our window. Here you can see a result of few clicks on “-” and “+” buttons.

Screenshot 05

6. Frozen timer

Timer needs this



use chrono::Local;


Enter fullscreen mode Exit fullscreen mode

Ok, that was hard! I made timer, but only for displaying a start time which is pretty useless. I need a real clock! And I show how to make it in the next chapter.
We need special label for our timer and String variable that contains time in this format %Y-%m-%d %H:%M:%S (you can choose your own format for example %m-%d-%Y which is confusing me a lot).
After all we are attaching our label_time gui::gtk::Label to our grid and getting beautiful frozen timer.



    let time = format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S"));
    let label_time = gtk::Label::new(None);
    label_time.set_text(&time);
    grid.attach(&label_time, 0, 6, 4, 1);


Enter fullscreen mode Exit fullscreen mode

Here it is – a clock that shows the correct time only when you start the program

Screenshot 06

7. This watch works!

Finally! We just need to create a variable and add a function (closure) to it. I have no idea how it actually works. It’s something like “get local time in this format each X seconds”. The first digit is the rate of timer updates glib::timeout_add_seconds_local(1, tick) is in seconds. The second is our variable that updates each second.
Not sure how glib::Continue(true) works.



    let tick = move || {
        let time = format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S"));
        label_time.set_text(&time);
        glib::Continue(true)
    };
    glib::timeout_add_seconds_local(1, tick);


Enter fullscreen mode Exit fullscreen mode

Now cargo run it. The application should look like this. Timer should update time every second

Screenshot 07

Screenshot 08

Now let's try to make this app using external markdown file.

CONCLUSION

I did my best at writing this post and I hope it was useful not only for me. Anyway it was a good experience: solve an issue and repeat the process from small useless app with just one element of GUI to big and… ok it still useless, but I learn something developing it.
Cheers
Good luck
Take care
And all the kind words that exist

💖 💪 🙅 🚩
antonov_mike
Antonov Mike

Posted on July 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Move data out of closure
rust Move data out of closure

July 22, 2022

Making GTK keyboard on Rust
rust Making GTK keyboard on Rust

July 17, 2022