Merhabalar, bu yazıda Pardus 23 işletim sisteminde GTK4’ü Elm mimarisi(Model, View, Update) ile birlikte kullanmamızı sağlayan Relm4 kütüphanesiyle Rust dilinde bir masaüstü uygulaması geliştireceğiz.


Programımızın nihai görüntüsü.

Kurulum

Geliştirmeye başlamadan önce gerekli kütüphaneleri ve Rust dilinin kurulumunu gerçekleştirelim.

GTK4

GTK4 kütüphanesiyle geliştirme yapabilmek için -dev paketini sistemimize kuralım:

sudo apt install libgtk-4-dev

Rust

Rust dilini sisteminize kurmak için önerilen resmi sitesinden indirmenizdir: https://www.rust-lang.org/tools/install

Hazırız! Projeyi oluşturalım

Öncelikle cargo ile yeni bir Rust projesi oluşturalım:

cargo new relm4-dersi

Daha sonra projemizin bağımlılıklarına relm4’ü ekleyelim (relm4, gtk4’e bağımlıdır, tekrar yazmaya gerek yok).

Örnek bir Cargo.toml dosyası:

[package]
name = "relm4-dersi"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
relm4 = "0.6"

# Yakında relm4 0.7 sürümü çıkacak, şuan 0.7.0-beta.2.

Projemizin bağımlılıklarını da ayarladıktan sonra içeriğini düzenlemeye başlayabiliriz.

Relm4 Uygulaması

Bir GTK4 + Relm4 uygulaması oluşturmak için src/main.rs dosyamızı aşağıdaki şekilde yapalım:

fn main() {
    let app = RelmApp::new("tr.org.pardus.relm4-dersi");
    app.run::<AppModel>(0);
}

AppModel bizim bir alt bölümde oluşturacağımız Model’imizin ismi ve parametredeki 0 değeri Relm uygulamasına vermek istediğimiz başlangıç parametresi. Bu parametre aşağıda tanımlayacağımız Component’in Init tipinden verilir.

App içinde kullanmak istediğimiz ve app’i koşturmaya başlatmadan önce elde ettiğimiz değerleri başlangıç parametresi vererek app’in içine aktarabiliriz.

::<AppModel> bir turbofish syntax örneğidir. Generic tip kullanan fonksiyonlarda bazen tipin ne olduğunu belirtmek için bu şekilde kullanılır(bir başka örnek: "42".parse::<i32>()).

app.run ile uygulamamızı çalıştırdık ve modelimizin AppModel tipinde olduğunu belirttik.

Elm mimarisi

https://www.tutorialspoint.com/elm/elm_architecture.htm

Model

İlk olarak uygulamamızda dinamik olarak değiştireceğimiz değerleri Model olarak tutacağımız struct’ımızı tanımlayalım.

Basit bir sayaç uygulaması yapacağımız için kullanıcı butona bastığında artacak/azalacak sayacın miktarını bir değişkende tutalım:

struct AppModel {
    counter: u8,
}

Message

Program çalışma esnasında modeldeki değerleri değiştirmek için view’den gelen Message’ları Update kısmında ele alıyoruz.

Model, Update ve View kısımları birbirinden tamamen bağımsızdır. Aralarında Message’lar ile haberleşirler.

Örneğin bir butona basıldığında Model’i güncellemek için buton bir Message gönderir. Bu farklı farklı manalar barındıran mesajların hepsini tek bir enumda tanımlayabiliriz:

enum Message {
    Increment, // Sayacı bir artır mesajı
    Decrement, // Sayacı bir azalt mesajı
}

Component

Component’ler Relm4’ün “building block”ları diyebiliriz. Bir widget listesi, mesaj ve model tipi, input output tipi gibi tüm bunların tanımlı olduğu nesneler Component’lerdir. Bir Relm4 componenti custom tanımlanabilen şu tipleri tutar:

  • Input: Component’lere gelen mesajların tipini belirtir. Örneğin bir buton basıldığında componente mesaj gelir ve component de update() fonksiyonunda buna göre bir işlem gerçekleştirir.
  • Output: Component’lerden diğer Component’lere mesaj göndermek için kullanılan tipi belirtir. Mesela iki component birbiriyle mesajlar ile haberleşmek istediğinde birinin Input tipine öbürünün Output tipi yazılmalıdır.
  • Init: Component oluşturulurken bir başlangıç değeri aktarmak istersek hangi tipte bir değer aktarmak istediğimizi belirtir.
  • Root: Component’in root(kök) GTK widgetinin tipini belirtir.
  • Widgets: Component’e ait ve update kısmında güncellemek istediğimiz widgetlerin tutulduğu struct’ın tipini belirtir.

Biz uygulamamız için bir tane Component kullanacağız, dolayısıyla sadece Input tipi bizim Message enum’ımız olacak ve Output tipi boş yani unit type () olacak.

Component Macrosu

Normalde yukarıda yazdığım değerleri Component traitini implement eden bir struct’a kendimiz elle tanımlamalıyız ancak bu işlemi bizim için kolaylaştıran bir macro var ve ben direkt olarak kod örneğini buradan vermek istiyorum. Detaylı bilgi için relm4 kitabına bakabilirsiniz.

AppModel modelimiz için Componentimiz:

#[relm4::component]
impl SimpleComponent for AppModel {
    type Init = u8;
    type Input = Message;
    type Output = ();

    // View
    view! {
        gtk::Window {
            set_title: Some("Simple app"),
            set_default_size: (300, 100),

            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                set_spacing: 5,
                set_margin_all: 5,

                gtk::Button {
                    set_label: "Increment",
                    connect_clicked => Message::Increment,
                },

                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked => Message::Decrement,
                },

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }
            }
        }
    }

    // Initialize the component.
    fn init(
        counter: Self::Init,
        root: &Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self>; {
        let model = AppModel { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        ComponentParts { model, widgets }
    }

    // Update
    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
        match msg {
            Message::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            Message::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }
}

Şimdi iyice anlamak için Component kodumuzu parça parça inceleyelim.

#[relm4::component]
impl SimpleComponent for AppModel {
    type Init = u8;
    type Input = Message;
    type Output = ();

Sadece Init, Input ve Output tiplerini implement etmemizi isteyen, Root ve Widgets’i view! macrosunda kendi oluşturup atayan SimpleComponent traitini implement ediyoruz. Eğer impl Component for AppModel tercih etseydik Root ve Widgets‘i de kendimiz yazmalıydık.

View

    view! {
        gtk::Window {
            set_title: Some("Simple app"),
            set_default_size: (300, 100),

            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                set_spacing: 5,
                set_margin_all: 5,

                gtk::Button {
                    set_label: "Increment",
                    connect_clicked => Message::Increment,
                },

                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked => Message::Decrement,
                },

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }
            }
        }
    }

300×100 boyutunda bir gtk::Window‘u oluşturduk ve içine main child olarak gtk::Box ekledik.

gtk::Box bir container ve içine birden fazla widget alıp onları ya alt alta veya yan yana dizer. Diğer dillerdeki HLayout veya VLayout yapılarına benzer.

gtk::Button bir buton ve üzerine tıklandığında connect_clicked ile atanmış fonksiyonu çalıştırır. Burada ise bir fonksiyon yerine direkt olarak Update aşamasına göndereceği mesajın ne olduğunu yazdık, bu yazım kolaylığını bize view! macrosu sağlıyor, yoksa açıkça yazmak istesek şöyle yazacaktık:

connect_clicked => move |_btn| { sender.input(Message::Increment); }

gtk::Label ise bir metin alanı widgeti. set_label fonksiyonunda label’in içeriğinde gözükecek yazıyı vermiş olduk.

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }

Yalnız bir fark var, üzerine koyduğumuz #[watch] etiketiyle set_label fonksiyonu model her değiştiğinde çağırılacak. Böylece model her değiştiğinde labelimiz otomatik olarak counter’ın değerini gösterecek.

Şimdi init() fonksiyonuna bir bakalım:

    // Initialize the component.
    fn init(
        counter: Self::Init,
        root: &Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let model = AppModel { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        ComponentParts { model, widgets }
    }

Component ilk defa oluşturulduğunda bu fonksiyon bir kere çalışır. Self::Init tipi bizim yukarıda tanımladığımız type Init = u8‘i gösteriyor. yani counter: u8 oluyor.

sender ise Component’imiz ile mesaj göndermek için kullandığımız şey diyebiliriz.

Burada model ve widgets’i oluşturduktan sonra ComponentParts struct’ı şeklinde oluşturduğumuz model ve widgetleri return ile geri döndürüyoruz.

Rust’da bir değeri return etmek için o değeri fonksiyonun son satırına noktalı virgülsüz şekilde yazabiliriz, daha önce return etmek için ise return degisken; şeklinde kullanmalıyız

widgets’i bizim için view_output!() macrosu view! macrosu içerisinde tanımladığımız ve bir isim atadığımız widgetler ile oluşturuyor(şuan label üzerinde program çalışırken değişiklik yapılacağı için sadece label var). Elle tanımlamak isteseydik şu şekilde yazabilirdik:

// Örnek el ile tanımlama

struct AppWidgets {
    label: gtk::Label,
}

///...

fn init(...) ... {
    let label = gtk::Label::new(Some(&format!("Counter: {}", model.counter)));

    ///..

    let widgets = AppWidgets { label };

    ComponentParts { model, widgets }
}

Programınızı macrosuz el ile yönetmek ve widgetleri el ile eklemek isterseniz Relm4 kitabındaki şu sayfaya bakabilirsiniz: https://relm4.org/book/stable/first_app.html

Programımızın Model, View, Update bölümlerindeki View kısmını bu fonksiyonda oluşturmuş olduk.

Update

Programımızın Model, View, Update bölümlerindeki Update kısmı işte burada gerçekleşiyor.

    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
        match msg {
            Message::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            Message::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }

update(), sender bir input mesajı gönderdiğinde çalışacak kodları içerir. Hangi mesajın geldiğini 2. parametrede msg değişkeniyle tutuyoruz. Mesajın tipi bizim type Input = Message; ile tanımladığımız tip, yani Message enum’ı. Yani msg: Message şeklinde tanımlı diyebiliriz.

Eğer Increment mesajı gelirse self.counter‘ı (yani AppModel struct’ından oluşturduğumuz bir nesnenin counter: u8 değişkenini) bir artırıyoruz. Bu kısım self.counter += 1 şeklinde de yazılabilirdi, fakat wrapping_add(1) ile eğer u8’in limiti yani 0 veya 255’ten taşarsa tekrar başa dönmesi sağlanıyor.

Eğer wrapping_add kullanılmazsa 255’e 1 eklendiğinde değer overflow olup program panic olur ve kapanır. Rust bunun gibi belirsizliklere yol açacak davranışların açıkça handle edilmesini istiyor. Bu yüzden += ile toplama işlemi varsayılan olarak wrapping değil.

Derleme ve Çalıştırma

Programımızı sadece derlemek için cargo build komutunu kullanabiliriz. Eğer programımızı debug bilgilerinden arınmış ve herkesle paylaşmak üzere derliyorsak cargo build --release şeklinde kullanmalıyız.

Programımızın tek dosyada bulunan tüm main.rs içeriği aşağıdaki gibidir:

use gtk::prelude::*;
use relm4::prelude::*;

// Model
struct AppModel {
    counter: u8,
}

#[derive(Debug)]
enum Message {
    Increment,
    Decrement,
}

#[relm4::component]
impl SimpleComponent for AppModel {
    type Init = u8;
    type Input = Message;
    type Output = ();


    // View
    view! {
        gtk::Window {
            set_title: Some("Simple app"),
            set_default_size: (300, 100),

            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                set_spacing: 5,
                set_margin_all: 5,

                gtk::Button {
                    set_label: "Increment",
                    connect_clicked => Message::Increment,
                },

                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked => Message::Decrement,
                },

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }
            }
        }
    }

    // Initialize the component.
    fn init(
        counter: Self::Init,
        root: &Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let model = AppModel { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        ComponentParts { model, widgets }
    }

    // Update
    fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
        match msg {
            Message::Increment => {
                self.counter += 1;
            }
            Message::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }
}

fn main() {
    let app = RelmApp::new("tr.org.pardus.relm4-dersi");
    app.run::<AppModel>(0);
}
Not: Program modüllere ayırmak ve arayüzü ayrı bir modülde ve ayrı bir dosyada tutmak, program büyüdükçe proje yönetmeyi daha da kolaylaştırır.

Programımızı derlemek ve çalışır halini görmek için:

cargo run

# veya

cargo run --release # Yayınlamak için, debug kodlarını içermez, düşük boyutlu ve daha hızlıdır.

Derlenmiş program çıktısını ./target/release/relm4-dersi yolunda bulabilirsiniz.

Programımızın nihai görüntüsü.
Programımızın çalışma videosu.

İşte bu kadardı!

Herhangi bir sorunuz olursa yazmaktan çekinmeyin. Yardım almak veya katkı sağlamak için Discord kanalımıza katılmayı unutmayın!

Detaylı bilgi için kaynaklar: