DEV Community

loading...
Cover image for Creating a GUI for a Rust application

Creating a GUI for a Rust application

henrybarreto profile image Henry Barreto Originally published at henrybarreto.dev ・7 min read

I guess majority the of beginner programmer wants to create something amazing and popular, and maybe, be famous and rich at some point. But, when he/she begins, the "black screen" from a terminal does not seem the new Facebook. So, what to do if want you to build a more user-friendly desktop's application, specifically? Building an application with a Graphical user interface!

To create the visual interface without work directly inside the code, what can be just a bunch of statements, we will use an application called Glade. It allows us to build easily GTK UI just dragging and drop components, generating an XML with the components, positions, information and so on about our interface.

Required crates are GTK and GIO. It just adds this in or Cargo.toml file from your project.

[dependencies.gtk]
version = "0.9.0"
features = ["v3_16"]

[dependencies.gio]
version = ""
features = ["v2_44"]
Enter fullscreen mode Exit fullscreen mode

The example application we will build is: Name This Color. With it, the user can choose a color and give it a name of her/his preference. Simple, but explainable.

Thereby NTC interface is. It seems too much XML to me... lets see as human should see.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkWindow" id="main_window">
    <property name="width_request">450</property>
    <property name="height_request">300</property>
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Name this color</property>
    <property name="resizable">False</property>
    <property name="window_position">center</property>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child>
      <object class="GtkFixed">
        <property name="width_request">450</property>
        <property name="height_request">300</property>
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <child>
          <object class="GtkEntry" id="color_name_entry">
            <property name="name">color_name_entry</property>
            <property name="width_request">166</property>
            <property name="height_request">40</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
          </object>
          <packing>
            <property name="x">145</property>
            <property name="y">170</property>
          </packing>
        </child>
        <child>
          <object class="GtkColorButton" id="color_selection">
            <property name="name">color_selection</property>
            <property name="width_request">100</property>
            <property name="height_request">80</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
          </object>
          <packing>
            <property name="x">175</property>
            <property name="y">45</property>
          </packing>
        </child>
        <child>
          <object class="GtkLabel" id="select_color_label">
            <property name="name">select_color_label</property>
            <property name="width_request">100</property>
            <property name="height_request">34</property>
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="label" translatable="yes">Select a color</property>
          </object>
          <packing>
            <property name="x">175</property>
            <property name="y">10</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="save_button">
            <property name="label" translatable="yes">Save</property>
            <property name="name">save_button</property>
            <property name="width_request">100</property>
            <property name="height_request">40</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
          </object>
          <packing>
            <property name="x">175</property>
            <property name="y">250</property>
          </packing>
        </child>
        <child>
          <object class="GtkLabel" id="name_color_label">
            <property name="name">name_color_label</property>
            <property name="width_request">100</property>
            <property name="height_request">41</property>
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="label" translatable="yes">Name this color</property>
          </object>
          <packing>
            <property name="x">175</property>
            <property name="y">135</property>
          </packing>
        </child>
        <child>
          <object class="GtkLabel" id="registered_color_label">
            <property name="name">registered_color_label</property>
            <property name="width_request">120</property>
            <property name="height_request">25</property>
            <property name="can_focus">False</property>
          </object>
          <packing>
            <property name="x">165</property>
            <property name="y">215</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>
Enter fullscreen mode Exit fullscreen mode

And thus it seems for a human:

Captura de tela de 2020-12-31 19-06-22.png

What does it?

So simple. We will just get the RGBA selected's color and the name, save it in a structure which has two vectors of Name and Color which is another structure defined with red, green, blue and alpha fields. After that, we will push the name and color captured to structure.

Showing the code is always the better explanation:

// src/ntc.rs
#[derive(Debug, PartialEq)]
pub struct Color {
    pub red: f64,
    pub green: f64,
    pub blue: f64,
    pub alpha: f64
}
pub struct NTC {
  pub names: Vec<String>,
  pub colors: Vec<Color>
}
impl NTC {
  pub fn new() -> Self {
    NTC {
      names: vec![],
      colors: vec![]
    }
  }
  pub fn save_color(&mut self, color: Color, name: String) -> Result<(), String> {
    if self.colors.contains(&color) || self.names.contains(&name) {
      Err("The color was already saved!".to_string())
    } else {
      self.colors.push(color);
      self.names.push(name);
      Ok(())
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The main code will be self explained:

// src/main.rs
use std::{cell::RefCell, path::Path, rc::Rc};

// gtk needs
use gtk::prelude::*;
use gio::prelude::*;

use ntc::Color;

mod ntc; // importing the ntc module

fn main() {
    gtk::init() // This function will initialize the gtk
    .expect("Could not init the GTK"); 
    // and if something goes wrong, it will send this message
    /*
    The documentation says about gtk::init and gtk::Application::new:
    "When using Application, it is not necessary to call gtk_init manually. 
    It is called as soon as the application gets registered as the 
    primary instance".
    It worth to check it.
    */

    // Here it defined a gtk application, the minimum to init an application
    // There are some caveats about this
    /*
       To build this interface, I have used a component GtkWindow as father of from 
       all others components, hence, it needed to create Gtk::Application inside 
       de code.

       If a GtkApplicationWindow had been to choose, it would not be necessary, 
       because it alraedy had a Gtk::Applicaiton "inside".
   */
    let application = gtk::Application::new(
        Some("dev.henrybarreto.name-this-color"), // Application id
        Default::default() // Using default flags
    ).expect("Could not create the gtk aplication");

    // The magic happens in this line
    // The ntc.glade is pushed into our code through a builder.
    // With this builder it is possible to get all components inside the XML from Glade
    let builder: gtk::Builder =  gtk::Builder::from_file(Path::new("ntc.glade"));

    // ----------------------------------------------------------|
    let colors_saved = Rc::new(RefCell::new(ntc::NTC::new()));// |
    // ----------------------------------------------------------|

    // when the signal connect_activate was sent, the application will get our
    // components for work
    application.connect_activate(move |_| {
    // All components from the ntc.glade are imported, until the one has not used to
    // for didactic propouses
    // the "method" get_object gets from the id.
        let main_window: gtk::Window = builder.get_object("main_window").expect("Could not get the object main_window");
        let save_button: gtk::Button = builder.get_object("save_button").expect("Could not get the save_button");
        let color_selection: gtk::ColorButton = builder.get_object("color_selection").expect("Could not get the color_selection");
        let color_name_entry: gtk::Entry = builder.get_object("color_name_entry").expect("Could not get the color_name_entry");
        //let _select_color_label: gtk::Label = builder.get_object("select_color_label").expect("Could not get the select_color_label");
        //let _name_color_label: gtk::Label = builder.get_object("name_color_label").expect("Could not get the name_color_label");
        let registered_color_label: gtk::Label = builder.get_object("registered_color_label").expect("Could not get the registeredd_color_label");

        let colors_saved = colors_saved.clone();

    // When the button was clicked...
    // The "main" logic happen here
        save_button.connect_clicked(move |_| {
            let color_rgba = color_selection.get_rgba(); // getting the color from the button
            let color: Color = Color { // setting manually color by color for didactic.
                red: color_rgba.red,
                green: color_rgba.green,
                blue: color_rgba.blue,
                alpha: color_rgba.alpha
            };
            let name = color_name_entry.get_text().to_string(); // getting name from the entry

            registered_color_label.set_visible(true); // Letting the label visible
            if let Ok(()) = colors_saved.borrow_mut().save_color(color, name) { // if the color is saved correctly
                registered_color_label.set_text("Registered!");
            } else { // when does it not
                registered_color_label.set_text("Already Registered!");
            }
        });

    // "event" when the close button is clicked
        main_window.connect_destroy(move |_| {
        // the gtk application is closed
            gtk::main_quit(); 
        });

        main_window.show(); // showing all components inside the main_window
    });

    application.run(&[]); // initializing the application
    gtk::main(); // initializing the gtk looping
}
Enter fullscreen mode Exit fullscreen mode

One detail in this code is important to comment: the use of Rc and RefCell. Knowing how Rust works and its system of memory management, it possibly to notice that move a variable definition through Fn trait function it does not a good idea and does not allow by the compiler.

I could admit that this blog was just to talk about Rc and RefCell, but I prefer does not say the truth.

Rc and RefCell

I already have commented about Rc in past blog, but for learning proposes, I will recap. Rc is used in Rust to makes possibles a single values has multiples owners; the keys word here are "single value" and "multiples owners".

Unlike Rc, RefCell holds a single value to one owner data; the keys words here are "single value" and "one owner data". Trying to explain it in a simple way, of course, could be problematic, therefore I highly recommend tasting a font with deep explanation, but for register too, I will try it.

I consider it a way to deceive the compiler about some borrowing rules, pushing this rules to be verified in run time. This borrowing rules has relation with mutation of data even when the references to this are immutable, for do that, It uses unsafe code internally, but the topic "unsafe" will be a full blog about it.

In our application, I have used this concept to create an outer NTC struct, get the color and name inside a closure and save it through a mutable reference to this outer structure. In others programming languages it would be trivial, but Rust has your own way to do the things... It pleasures me.

The project ends thus:

.
├── Cargo.lock
├── Cargo.toml
├── ntc.glade
└── src
    ├── main.rs
    └── ntc.rs
Enter fullscreen mode Exit fullscreen mode

Simple, is not it?

How does the application look like?

Captura de tela de 2020-12-31 19-07-12.png

When a register is added...

Captura de tela de 2020-12-31 19-12-24.png

When the either color or name already exist inside the "database".

Captura de tela de 2020-12-31 19-12-36.png

This was a more personal blog, at this one I tried to be more "i", contrary to what I have been trying to do, but it is still like a journal about what learned and to be as an abstract to futures reviewers.

Thanks for reading, and feel free to comment, correct me or just say a hi. I hope it helps someone.

Useful links:

Discussion

pic
Editor guide