Adding Widgets To GTK3

Now that we have our window, which currently does nothing but display a window itself, it's time to start generating some widgets to attach to the window. A widget is simply a GUI object that attaches to a window, like a text box or a label.

Create a Scrolled Window

The information that we are collecting will be displayed in a scrolled window, and because a gtk::Window can only contain one widget, that widget is going to be a gtk::ScrolledWindow. The new() method of a gtk::ScrolledWindow takes two input variables which are used to control alignment. We aren't going to set any alignment options so we will simply tell them None.

let scrolled_window = gtk::ScrolledWindow::new(None, None).unwrap();

It would also be a good idea to set a minimum width for our scrolled_window widget.

scrolled_window.set_min_content_width(600);

The new scrolled_window variable is useless by itself with no widgets to attach to it, however, so we need to create all the widgets we need for each of our articles. However, just like a gtk::Window, a gtk::ScrolledWindow will only allow one widget to be attached to it, so we will use a gtk::Box as a container for all of the articles.

Create a Container

A gtk::Box can have any number widgets attached to them, and are laid out either horizontally or vertically with gtk::Orienation::Horizontal and gtk::Orientation::Vertical respectively. You may also define the margins between widgets, but it's generally best to leave this at 0.

let container = gtk::Box::new(gtk::Orientation::Vertical, 0).unwrap();

Collecting Articles

Before we can start the creation of widgets for our GUI, we need to collect the list of Articles.

let articles = Article::get_articles(&homepage::offline());

Creating Widgets From the Vec<Article>

Each article will consist of a gtk::Box that contains the entire Article. First is a gtk::LinkButton containing both the Title and URL. The details and summaries will be contained within their own respective gtk::TextView widgets. To keep our launch() function cleaned up, we will create a new function specifically for the creation of the list of articles.

fn generate_article_widgets(container: &gtk::Box, articles: &Vec<Article>) {
    for article in articles {
        // Code Here
    }
}

This function will take the container variable we created earlier, along with the Vec of Articles as input, and simply iterate over each element -- attaching a new GUI widget with each loop.

Obtaining the Title and URL Widget

Let's start by creating a widget for the title and link, where we will create a gtk::LinkButton. The gtk::LinkButton::new_with_label() function takes two inputs: the URL and a label as &str types. By default, this will be centered so we can use the set_halign() method to set a left alignment with gtk::Align::Start. We can get the URL via article.link and the label with article.title.

for article in articles {
    let url = format!("https://phoronix.com/{}", article.link);
    let title_and_url = gtk::LinkButton::new_with_label(&url, &article.title).unwrap();
    title_and_url.set_halign(gtk::Align::Start);
}

Obtaining Details

The next step is creating a gtk::TextView widget containing the details obtained from article.details. Like our title widget, we also want to left-align the text, and this time also set a left and right margin. The set_left_margin() and set_right_margin() may be used to specify how many pixels to use for the margins of the text inside the gtk::TextView. We also do not want users to be able to edit the text, so we will disable editing with set_editable(false). You can't set the text during creation of the widget though, so that will have to be defined after creation using get_buffer() and set_text().

    let details = gtk::TextView::new().unwrap();              // Create the TextView Widget
    details.set_halign(gtk::Align::Start);                    // Set a left alignment for the text
    details.set_left_margin(10);                              // 10 pixel left margin
    details.set_right_margin(10);                             // 10 pixel right margin
    details.set_editable(false);                              // Disable text editing
    details.get_buffer().unwrap().set_text(&article.details); // Set the text in the widget

Obtaining Summaries

Now all we need to get is a widget for the summaries. The process is very much the same, but we will use a few extra features for setting how the widget operates, namely defining a wrap mode with set_wrap_mode() and setting the number of pixels above and below lines with set_pixels_above_lines() and set_pixels_below_lines().

    let summary = gtk::TextView::new().unwrap();              // Create the TextView Widget
    summary.set_wrap_mode(gtk::WrapMode::Word);               // Wrap Lines By Words
    summary.set_left_margin(10);                              // 10 pixel left margin
    summary.set_right_margin(10);                             // 10 pixel right margin
    summary.set_pixels_above_lines(10);                       // 10 pixels above summmary
    summary.set_pixels_below_lines(10);                       // 10 pixels below summary
    summary.set_editable(false);                              // Disable text editing
    summary.get_buffer().unwrap().set_text(&article.summary); // Set the text in the widget

Add Widgets to Container

Now all we just have to do is add these widgets to the container and we are pretty much finished with this function, albeit we have yet to perform any kind of coloring to these widgets.

container.add(&title_and_url);
container.add(&details);
container.add(&summary);

Adding Widgets to the Window and Displaying Them

Adding the container to our scrolled_window, and the scrolled_window to the window should be pretty straightforward.

scrolled_window.add(&container);
window.add(&scrolled_window);
window.show_all();

Quitting Program When Esc Is Pressed

If you would like to be able to program the window to quit the program when the Esc key is pressed, we can make use of the connect_key_press_event() method for the gtk::Window type. This will take itself and the key that was pressed when an event is triggered, and is used to match the key to any code of your choice that you want to assign to that key press.

// Define actions on key press
window.connect_key_press_event(move |_, key| {
    match key.keyval as i32 {
        key::Escape => gtk::main_quit(),
        _ => ()
    }
    gtk::signal::Inhibit(false)
});

Review

phoronix_gui.rs

use article::Article;
use homepage;
use gtk;
use gtk::traits::*;
use gdk::ffi::GdkRGBA;
use pango;

pub fn launch() {
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));

    // Create widgets for the articles
    let container = gtk::Box::new(gtk::Orientation::Vertical, 0).unwrap();
    let articles = Article::get_articles(&homepage::online());
    generate_article_widgets(&container, &articles);

    // Insert the articles into a scrolled window
    let scrolled_window = gtk::ScrolledWindow::new(None, None).unwrap();
    scrolled_window.set_min_content_width(600);
    scrolled_window.add(&article_box);

    // Add the scrolled window to a main window
    let window = gtk::Window::new(gtk::WindowType::Toplevel).unwrap();
    configure_window(&window);
    window.add(&scrolled_window);
    window.show_all();

    // Define actions on key press
    window.connect_key_press_event(move |_, key| {
        match key.keyval as i32 {
            key::Escape => gtk::main_quit(),
            _ => ()
        }
        gtk::signal::Inhibit(false)
    });

    gtk::main();
}

// configre_window configures the given window.
fn configure_window(window: &gtk::Window) {
    window.set_title("Phoronix Reader");
    let (width, height) = (600, 500);
    window.set_default_size(width, height);
    window.connect_delete_event(|_,_| {
        gtk::main_quit();
        gtk::signal::Inhibit(true)
    });
}

// generate_article_widgets takes a vector of articles as well as a gtk::Box and fills up the gtk::Box
// with widgets generated from each article
fn generate_article_widgets(article_box: &gtk::Box, articles: &Vec<Article>) {
    for article in articles {
        // Creates the title as a gtk::LinkButton for each article
        let url = format!("https://phoronix.com/{}", article.link);
        let title_and_url = gtk::LinkButton::new_with_label(&url, &article.title).unwrap();
        title_and_url.set_halign(gtk::Align::Start);
        title_and_url.set_margin_start(0);

        // Details of the article inside of a gtk::TextView
        let details = gtk::TextView::new().unwrap();
        details.set_halign(gtk::Align::Start);
        details.set_left_margin(10);
        details.set_right_margin(10);
        details.set_editable(false);
        details.get_buffer().unwrap().set_text(&article.details);

        // Summary of the article inside of a gtk::TextView
        let summary = gtk::TextView::new().unwrap();
        summary.set_wrap_mode(gtk::WrapMode::Word);
        summary.set_left_margin(10);
        summary.set_right_margin(10);
        summary.set_pixels_above_lines(10);
        summary.set_pixels_below_lines(10);
        summary.set_editable(false);
        summary.get_buffer().unwrap().set_text(&article.summary);

        // Attach the title+url, details and summary to the article_box
        container.add(&title_and_url);
        container.add(&details);
        container.add(&summary);
        container.add(&gtk::Separator::new(gtk::Orientation::Horizontal).unwrap());
    }
}