Getting started with Rust + GTK4

Getting started with Rust + GTK4

Being new to Rust, I'm starting with various simple examples to understand some of the dynamics. After looking into the Tauri methods, I concluded that GTK or QT might be the better path for traditional desktop applications, while Tauri and others should be used if you want a modern, custom-stylized approach. I'm not a CSS expert, so this might be a quicker path for me at the moment.

Examples are generally coming from https://github.com/gtk-rs/gtk4-rs/tree/master/examples

Install GTK4 for Windows

I followed the following URL but included specific instructions for convenience.

https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html

# -------------
# GTK4 install
# -------------

# Set Rust toolchain to MSVC
# Note that Visual Studio 2022 Community was already installed on my system
rustup default stable-msvc

# install build dependencies
python -m pip install --user pipx
python -m pipx ensurepath
pipx install gvsbuild

# Follow the gvsbuild docs to build GTK 4. 
# https://github.com/wingtk/gvsbuild#development-environment
gvsbuild build gtk4

# add the following to PATH
C:\gtk-build\gtk\x64\release\bin

# -------------
# project setup
# -------------
cargo new hellorustgtk4
cd hellorustgtk4
code .

# Find out the GTK 4 version on your machine by running
# it returned "4.10.4"
pkg-config --modversion gtk4

# Use this information to add the gtk4 crate to your dependencies in Cargo.toml. At the time of this writing the newest version is 4.10.
cargo add gtk4 --rename gtk --features v4_10

# build
cargo build

# build and run
cargo run

Example 1 - Basic

I believe that the following example originated from https://github.com/gtk-rs/gtk4-rs/tree/master/examples/basics.

extern crate gtk;

use gtk::glib;
use gtk::prelude::*;

fn main() -> glib::ExitCode {
    let application =
        gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default());
    application.connect_activate(build_ui);
    application.run()
}

fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);

    window.set_title(Some("First GTK Program"));
    window.set_default_size(350, 70);

    let button = gtk::Button::with_label("Click me!");

    window.set_child(Some(&button));

    window.present();
}

Example 2 - Dialog (Failure)

I attempted this example, but couldn't figure out the ControlFlow dependency build error.

https://github.com/gtk-rs/gtk4-rs/blob/master/examples/dialog/main.rs

Example 3 - Clipboard

This example didn't require anything special

https://github.com/gtk-rs/gtk4-rs/blob/master/examples/clipboard/main.rs

I had to download the image and change the path, per the following.

use glib::clone;
use gtk::prelude::*;
use gtk::{gdk, gio, glib};

fn main() -> glib::ExitCode {
    let application = gtk::Application::new(
        Some("com.github.gtk-rs.examples.clipboard"),
        Default::default(),
    );
    application.connect_activate(build_ui);
    application.run()
}

fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::builder()
        .application(application)
        .title("Clipboard")
        .default_width(660)
        .default_height(420)
        .build();

    let display = gdk::Display::default().unwrap();
    let clipboard = display.clipboard();

    let container = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .margin_top(24)
        .margin_bottom(24)
        .margin_start(24)
        .margin_end(24)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .spacing(24)
        .build();

    // The text copy/paste part
    let title = gtk::Label::builder()
        .label("Text")
        .halign(gtk::Align::Start)
        .build();
    title.add_css_class("title-2");
    container.append(&title);

    let text_container = gtk::Box::builder()
        .halign(gtk::Align::Center)
        .orientation(gtk::Orientation::Horizontal)
        .spacing(24)
        .build();

    let from_entry = gtk::Entry::builder()
        .placeholder_text("Type text to copy")
        .build();
    text_container.append(&from_entry);

    let copy_btn = gtk::Button::with_label("Copy");
    copy_btn.connect_clicked(clone!(@weak clipboard, @weak from_entry => move |_btn| {
        let text = from_entry.text();
        clipboard.set_text(&text);
    }));
    text_container.append(&copy_btn);

    let into_entry = gtk::Entry::new();
    text_container.append(&into_entry);

    let paste_btn = gtk::Button::with_label("Paste");
    paste_btn.connect_clicked(clone!(@weak clipboard, @weak into_entry => move |_btn| {
        clipboard.read_text_async(gio::Cancellable::NONE, clone!(@weak into_entry => move|res| {
            if let Ok(Some(text)) = res {
                into_entry.set_text(&text);
            }
        }));
    }));
    text_container.append(&paste_btn);
    container.append(&text_container);

    // The texture copy/paste part
    let title = gtk::Label::builder()
        .label("Texture")
        .halign(gtk::Align::Start)
        .build();
    title.add_css_class("title-2");
    container.append(&title);

    let texture_container = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .halign(gtk::Align::Center)
        .spacing(24)
        .build();

    let file = gio::File::for_path("./asset.png");
    let asset_paintable = gdk::Texture::from_file(&file).unwrap();

    let image_from = gtk::Image::builder()
        .pixel_size(96)
        .paintable(&asset_paintable)
        .build();
    texture_container.append(&image_from);
    let copy_texture_btn = gtk::Button::builder()
        .label("Copy")
        .valign(gtk::Align::Center)
        .build();
    copy_texture_btn.connect_clicked(clone!(@weak clipboard, @weak image_from => move |_btn| {
        let texture = image_from.paintable().and_downcast::<gdk::Texture>().unwrap();
        clipboard.set_texture(&texture);
    }));
    texture_container.append(&copy_texture_btn);

    let image_into = gtk::Image::builder()
        .pixel_size(96)
        .icon_name("image-missing")
        .build();
    texture_container.append(&image_into);
    let paste_texture_btn = gtk::Button::builder()
        .label("Paste")
        .valign(gtk::Align::Center)
        .build();
    paste_texture_btn.connect_clicked(clone!(@weak clipboard => move |_btn| {
        clipboard.read_texture_async(gio::Cancellable::NONE, clone!(@weak image_into => move |res| {
            if let Ok(Some(texture)) = res {
                image_into.set_paintable(Some(&texture));
            }
        }));
    }));
    texture_container.append(&paste_texture_btn);
    container.append(&texture_container);

    window.set_child(Some(&container));
    window.present();
}

Example 4 - Text Viewer

https://github.com/gtk-rs/gtk4-rs/blob/master/examples/text_viewer/main.rs

This example is a simple notepad-like application and demonstrates the GtkBuilder XML descriptions approach.

main.rs

use gtk::prelude::*;

use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;

use gtk::{gio, glib, Application, ApplicationWindow, Builder, Button, FileDialog, TextView};

fn main() -> glib::ExitCode {
    let application = Application::new(
        Some("com.github.gtk-rs.examples.text_viewer"),
        Default::default(),
    );
    application.connect_activate(build_ui);
    application.run()
}

pub fn build_ui(application: &Application) {
    let ui_src = include_str!("text_viewer.ui");
    let builder = Builder::new();
    builder
        .add_from_string(ui_src)
        .expect("Couldn't add from string");

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

    open_button.connect_clicked(glib::clone!(@weak window, @weak text_view => move |_| {

        let dialog = FileDialog::builder()
            .title("Open File")
            .accept_label("Open")
            .build();

        dialog.open(Some(&window), gio::Cancellable::NONE, move |file| {
            if let Ok(file) = file {
                let filename = file.path().expect("Couldn't get file path");
                let file = File::open(filename).expect("Couldn't open file");

                let mut reader = BufReader::new(file);
                let mut contents = String::new();
                let _ = reader.read_to_string(&mut contents);

                text_view.buffer().set_text(&contents);
            }
        });
    }));

    window.present();
}

text_viewer.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkApplicationWindow" id="window">
    <property name="title" translatable="yes">Text File Viewer</property>
    <property name="default-width">400</property>
    <property name="default-height">480</property>
    <child>
      <object class="GtkBox" id="v_box">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkBox">
            <child>
              <object class="GtkButton" id="open_button">
                <property name="label" translatable="yes">Open</property>
                <property name="icon-name">document-open-symbolic</property>
                <property name="tooltip-text" translatable="yes">Open</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkScrolledWindow" id="scrolled_window">
            <property name="hexpand">True</property>
            <property name="vexpand">True</property>
            <child>
              <object class="GtkTextView" id="text_view"/>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

If you want to wrap text in this example, you can set the TextView property.

I used the following for reference:

              <object class="GtkTextView" id="text_view">
                <property name="wrap-mode">GTK_WRAP_WORD</property>
              </object>

Conclusion

This seems to have some potential.

The XML approach appears comparable to C# WPF approach.

It is unclear how much open-source is available for custom functionality. The reactive framework approach with Tauri is sure to have more readily available open-source solutions.

Source: https://github.com/ericjameszimmerman/rustbasics