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
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.
İş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:
- Relm4 resmi kitabı: https://relm4.org/book/stable/
- Relm4 resmi sitesi: https://relm4.org/
- Relm4 resmi örnekleri: https://github.com/Relm4/Relm4/blob/main/examples/simple.rs
- Relm4 dokümantasyonu: https://docs.rs/relm4/latest/relm4/
- gtk4: https://gtk.org/
- Elm mimarisi: https://guide.elm-lang.org/architecture/