DEV Community

Cover image for Making external keyboard on Rust
Antonov Mike
Antonov Mike

Posted on

Making external keyboard on Rust

INTRO

Ok we made pure GTK keyboard with GTK buttons in previous post. Maybe it is a good idea to compare two ways of developing a small keyboard.
Now we gonna make the same using external markdown file, let’s call it grid.ui and it is too huge to post it here. You can just grab it from my github.
If you are rookie like me you can ask “why should we use extra file if we can use GTK only?” I guess we may have at least two reasons:
1) It helps to move some part of GUI stuff to the markdown file to make our code easy to read.
2) Some functions work different in different cases, and it’s interesting how we can use it.

By the way: pure GTK buttons are not scalable by default, but this ones are.

Some things I’ve explained in the previous post, so I just show you what we need in our project.

Open Cargo.toml and add list of crates

[dependencies]
gtk = "0.15.5"
glib = "0.15.10"
chrono = "0.4.19"
Enter fullscreen mode Exit fullscreen mode

We won’t change main.rs file

use gtk::prelude::*;

mod buttons;

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

    application.connect_activate(buttons::build_ui);
    application.run();
}
Enter fullscreen mode Exit fullscreen mode

We are starting buttons.rs with only few lines of code.

use gtk::prelude::*;
use gtk::{ApplicationWindow, Builder};

pub fn build_ui(application: &gtk::Application) {
    let glade_src = include_str!("grid.ui");
    let builder = Builder::from_string(glade_src);

    let window: ApplicationWindow = builder.object("window").expect("Couldn't get window");
    window.set_application(Some(application));

    // We are gonna add our buttons and functions here

    window.show_all();
}
Enter fullscreen mode Exit fullscreen mode

If you cargo run it you will see all the elements attached to the grid, because all of them are created in a markdown src/grid.ui file. All we need – just connect this file

let glade_src = include_str!("grid.ui");
Enter fullscreen mode Exit fullscreen mode

Then send glade_src to Builder as a string slice &str.

let builder = Builder::from_string(glade_src);
Enter fullscreen mode Exit fullscreen mode

And connect to ApplicationWindow

let window: ApplicationWindow = builder.object("window").expect("Couldn't get window");
Enter fullscreen mode Exit fullscreen mode

One more time:
1) Create markdown file with all elements you need
2) Bind it to variable
3) Send this variable to GTK builder
4) Connect it with ApplicationWindow

Let’s look at pure GTK again (do not add this code into our current project)

    let window = gtk::ApplicationWindow::new(application);
    window.set_title("Studying GTK");
    window.set_default_size(200, 120);
    let grid = gtk::Grid::builder()
        .margin_start(7)
        .margin_end(7)
        .margin_top(7)
        .margin_bottom(7)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .row_spacing(7)
        .column_spacing(7)
        .build();
    window.set_child(Some(&grid));
Enter fullscreen mode Exit fullscreen mode

See the difference? We have to create window, then make grid and connect to our window. Let’s get back to our current project.
We are using markdown file and it should look like on the screenshot below. This is a bunch of useless buttons because Rust doesn’t know they exist. As I said before – it’s just a string slice of our markdown file content. We have to explain Rust how to use it. And here we need class id.

<object class="GtkButton" id="button0">
Enter fullscreen mode Exit fullscreen mode

Let’s look at it closer in the next chapter.

Screenshot №1

1. Sliding button

Update your dependencies and let’s connect a grid.

use gtk::glib;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Builder};
use gtk::{Button, Grid};
Enter fullscreen mode Exit fullscreen mode

First we need a variable of type Grid. It calls gtk::Builder and method object() which receives some “grid”. What is that?

    let grid: Grid = builder.object("grid").expect("Couldn't get grid");
Enter fullscreen mode Exit fullscreen mode

This is the class and id. Class tells what kind of element it is. Id is a unique identifier of this very element. For example it can be element of type ”GtkGrid” with id “grid”.

<object class="GtkGrid" id="grid">
Enter fullscreen mode Exit fullscreen mode

As you remember we told Builder that our GUI stored inside grid.ui file

let glade_src = include_str!("grid.ui");
let builder = Builder::from_string(glade_src);
Enter fullscreen mode Exit fullscreen mode

Now gtk::Builder can find element by it’s id inside the markdown file and bind function to it. And we gonna do it right now.
Let’s create button0 and call it by id “button0”.

    let button0: Button = builder.object("button0").expect("Couldn't get button0");
Enter fullscreen mode Exit fullscreen mode

Now we can add function to it by calling connect_clicked().

    button0.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);
    }));

    window.show_all();
}
Enter fullscreen mode Exit fullscreen mode

We are using glib::clone! To make a clone and @weak or @strong (looks like both do the same in this case) to tell Rust what type of clone we want to use. Then we are moving type button into closure and returning it at the end with a new position in the grid.
Here we are declaring two variables: left_attach and new_left_attach. After starting the application the first variable contains 1.
The value of new_left_attach depends on if else condition. Each click increases left_attach by 1. As soon as it getting 2, else condition makes it equal 0. Try to change conditions,

<property name="left_attach">1</property>
Enter fullscreen mode Exit fullscreen mode

and button0 width.
Ok, now Button 0 slides right each time we click on it.

2. Numeric buttons

I told you gtk::Builder can bind function to element’s id but it also can send data. Let’s add numeric buttons and made them change their own labels.
Connect buttons using their id

    let button1: Button = builder.object("button1").expect("Couldn't get button1");
    let button2: Button = builder.object("button2").expect("Couldn't get button2");
    let button3: Button = builder.object("button3").expect("Couldn't get button3");
    let button4: Button = builder.object("button4").expect("Couldn't get button4");
    let button5: Button = builder.object("button5").expect("Couldn't get button5");
    let button6: Button = builder.object("button6").expect("Couldn't get button6");
    let button7: Button = builder.object("button7").expect("Couldn't get button7");
    let button8: Button = builder.object("button8").expect("Couldn't get button8");
    let button9: Button = builder.object("button9").expect("Couldn't get button9");
Enter fullscreen mode Exit fullscreen mode

Add some function that gonna set button’s label to numerical letter after button has been clicked on. Notice we don’t have to use curly braces in this case. Also we don’t need glib::clone! because we don’t move any data inside the closure. Method set_label() sets new label.

    button1.connect_clicked( move |button| button.set_label("I") );
    button2.connect_clicked( move |button| button.set_label("II") );
    button3.connect_clicked( move |button| button.set_label("III") );
    button4.connect_clicked( move |button| button.set_label("IV") );
    button5.connect_clicked( move |button| button.set_label("V") );
    button6.connect_clicked( move |button| button.set_label("VI") );
    button7.connect_clicked( move |button| button.set_label("VII") );
    button8.connect_clicked( move |button| button.set_label("VIII") );
    button9.connect_clicked( move |button| button.set_label("IX") );
Enter fullscreen mode Exit fullscreen mode

Run and behold! Numeric buttons 1-9 change their labels after first click.

Screenshot №2

3. Quit button

Another dependency

use glib::clone;
Enter fullscreen mode Exit fullscreen mode

In this case @weak and @strong are the same. Probably. Still not sure how it works.
Declare quit_button with type of Button. Pay attention at object() name! Here we are calling object by it’s id. In previous chapter |button| was a type not a data. But now we need glib::clone! because we move window inside the closure to destroy it.

    let quit_button: Button = builder.object("quit_button").expect("Couldn't get quit_button");
    quit_button.connect_clicked(clone!(@weak window => move |_|
        unsafe {
            window.destroy()
        }
    ));
Enter fullscreen mode Exit fullscreen mode

Let’s compare it with no markdown file project from my previous post (do not add this code into our current project). As you can see, here we are creating button themselves.

let quit_button = gtk::Button::with_label("Quit");
Enter fullscreen mode Exit fullscreen mode

Connecting function,which is completely the same as before…

    quit_button.connect_clicked(clone!(@weak window => move |_| 
        unsafe {
            window.destroy()
        }
    ));
Enter fullscreen mode Exit fullscreen mode

and attaching it to grid.

grid.attach(&quit_button, 3, 1, 1, 4);
Enter fullscreen mode Exit fullscreen mode

The difference is: 1) declaring 2) attaching

4. Updating label

Now make label receiving data by clicking button.
Hope you have noticed when we are using external markdown file, type annotations needed. We have to tell to compiler – this is a GTK object of type Label.

let counter_label: gtk::Label = builder.object("GtkLabel_1").expect("Couldn't get GtkLabel_1");
Enter fullscreen mode Exit fullscreen mode

Declaring minus_button and plus_button is the same as before, nothing special.

let minus_button: Button = builder.object("minus").expect("Couldn't get minus");
let plus_button: Button = builder.object("plus").expect("Couldn't get plus");
Enter fullscreen mode Exit fullscreen mode

Functions would be hundred percent the same as we made in pure GTK project.

    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));
    }));
    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));
    }));
Enter fullscreen mode Exit fullscreen mode

Now buttons work and send data to label.

Screenshot №3

5. Timer

Bring a new path into scope.

use chrono::Local;
Enter fullscreen mode Exit fullscreen mode

At the end list of paths looks like this.

use gtk::glib;
use glib::clone;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Builder};
use gtk::{Button, Grid};
use chrono::Local;
Enter fullscreen mode Exit fullscreen mode

Declare time variable like we did it on a previous project.

let time = format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S"));
Enter fullscreen mode Exit fullscreen mode

But this part is different!

let label_time: gtk::Label = builder.object("GtkLabel_2").expect("Couldn't get GtkLabel_2");
label_time.set_text(&time);
Enter fullscreen mode Exit fullscreen mode

Just compare with the similar part in previous project (do not add this code into our current project).

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

This is the same as in the case with pure GTK grid.

    let tick = move || {
        let time = format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S"));
        label_time.set_text(&time);
        // What is this?
        glib::Continue(true)
    };
    glib::timeout_add_seconds_local(1, tick);
Enter fullscreen mode Exit fullscreen mode

Now cargo run it and you will see this. All the buttons work fine. Just one little detail left…

Screenshot №4

6. Entry doesn’t work!

As documented Entry should look like this, but unfortunately, I wasn’t able to get it to work

<object class="GtkEntry">
  <attributes>
    <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
    <attribute name="background" value="red" start="5" end="10"/>
  </attributes>
</object>
Enter fullscreen mode Exit fullscreen mode

I spend some time trying to solve this riddle. I'll probably come back to this issue later.

CONCLUSION

There are still a few things here that I don't quite understand yet. And also there are some ideas that I can't implement right now. For example a built-in display or a save file button. But I think I will gradually figure this out as well.
Hope this post was useful and educational
Take care

Top comments (0)