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: >k::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: >k::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(©_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(©_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:
https://developer-old.gnome.org/gtk4/stable/GtkTextView.html
https://developer-old.gnome.org/gtk4/stable/GtkTextView.html#GtkWrapMode
<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.