いまさらGTKとRustでLinuxデスクトップアプリ入門

# 概要
いまさらですが、はじめてLinuxのデスクトップアプリを作成したので、備忘の意味も含めてその手順を書きます。

### 使うツール
GUIツールキットはGTK+を使います。Qtでも良かったのですが、環境整備や[チュートリアル](https://developer.gnome.org/gtk3/stable/gtk-getting-started.html)がシンプルで手っ取り早く始められそうな感じがしたのでGTK+を選びました。

また、言語はRustにしました。Cだと目新しさもないので、他の言語を使って書きたかったからです。
また、GTK+の[バイディングのサポート状況](https://www.gtk.org/language-bindings.php)を見て、GTK+3を完全にサポートしている言語のうちVMを使わない言語で
今後も使われていきそうなものを選びました。
(完全に直感です。C++は学習自体辛そうなので避けました。)

### 前提
この記事を読むにあたり、GTK+の知識は不要ですが、Rustの入門程度は済ませておいた方が良いです。
Rust自体の説明は殆どしないつもりです。[^1]

[^1]: 自分自身GTK+とRustのチュートリアルを読みながらこの記事のアプリを作成したので、Rustに対する理解は浅いです。。

### 作るアプリ
最初なのでシンプルなテキストエディタを作ります。Windowsのメモ帳と全く同じだとつまらないのでタブ型のUIにします。
アプリ名はVanilla Text[^2]としておきます。

[^2]: Vanilla JSを真似し「何もトッピングされていないバニラアイスのような」という意味を込めて。

![PrtSc_VanillaText.png](https://qiita-image-store.s3.amazonaws.com/0/55971/b36111e1-25da-b174-c399-7e7683f54443.png)



# 環境
本記事では以下の環境を使っています。

OS: Ubuntu 16.04 LTS
言語: Rust 1.16.0 (https://www.rust-lang.org/)
GTK+ライブラリ: GTK+ 3.18 (https://www.gtk.org/)
Rustバインディング: Gtk-rs 0.2.0 (http://gtk-rs.org/)

Ubuntuの場合、Rustは公式サイトからダウンロードしてインストールするのが簡単です。
GTK+は、以下のようにaptでインストールできます。

```bash
~$ sudo apt install libgtk-3-dev
```

Gtk-rsは、後ほどCargoで自動的にインストールします。

# Hello Gtk-rs
では早速始めます。
まずは、Cargoを使って作業環境を作成します。アプリケーションを作成するのでbinary template(`--bin`)を指定します。

```bash
~$ cargo new --bin vanilla_text
     Created binary (application) `vanilla_text` project
~$ cd vanilla_text/
~/vanilla_text$ tree
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files
```

次に、Gtk-rsを使うために、Cargo.tomlに依存関係を記述します。
Gtk-rsのバージョンは0.2.0で、GTK+はバージョン3.16相当の機能を使う為、以下のようにします。

```toml:Cargo.toml
[package]
name = "vanilla_text"
version = "0.1.0"
authors = ["Your Name <Your Email Address>"]

[dependencies]
gio= "0.2.0"

[dependencies.gtk]
version = "0.2.0"
features = ["v3_16"] 
```

それでは、画面にウィンドウを表示させてみます。
main.rsを以下のように編集します。

```rust:main.rs
extern crate gtk;
extern crate gio;

use gtk::{ WidgetExt, WindowExt };
use gio::{ ApplicationExt };

fn main() {
    //アプリケーションを生成する
    match gtk::Application::new("com.github.koji-m.vanilla_text", gio::APPLICATION_HANDLES_OPEN) {
        Ok(app) => {
            //アプリケーションへ、activateシグナルに対するハンドラを設定する
            app.connect_activate(|app| {
                //ウィンドウを生成する
                let win = gtk::ApplicationWindow::new(&app);
                //ウィンドウのタイトルに表示する文字列を設定する
                win.set_title("Hello Gtk-rs");
                //ウィンドウとその中身全てを可視状態にする
                win.show_all();
            });

            //アプリケーションを開始、アプリケーションへactivateシグナルがエミットされる
            app.run(&[""]);
        },
        Err(_) => {
            println!("Application start up error");
        }
    };
}
```

Cargoでアプリケーションをコンパイル・実行します。

```bash
~/vanilla_text$ cargo run
```

以下のように表示されれば成功です。
![PrtSc_Hello_Gtk-rs.png](https://qiita-image-store.s3.amazonaws.com/0/55971/83682b9f-bb48-da34-6ee2-a835ed5b7ee2.png)

GTK+以外にも、`extern crate gio`でGIO crateを利用しています。GIOは、GNOMEのVFS APIやデスクトップアプリケーションの為のAPIを提供するものです。GTK+は、このGIOやオブジェクトシステムを提供するGObjectなどのライブラリの上に構築されているライブラリです。

`main`関数では最初に、`gtk::Application::new()`で`GtkApplication`のインスタンスを生成しています。この`GtkApplication`がアプリケーション自体を表すクラスとなります。

ここで、クラスという表現を使いましたが、GTK+はクラスとその継承を使ったオブジェクト指向ベースのライブラリとなっています。

GTK+のAPIリファレンスを見ると、`GtkApplication`のクラス階層は以下のようになっています。

```
GObject
└── GApplication
    └── GtkApplication
```

`GApplication`は、`GtkApplication`などのアプリケーションを表すクラスの基礎となる機能を持ったGIOのクラスとなります。

コードの説明に戻ります。`gtk::Application::new()`は、第一引数にアプリケーションID(アプリケーションを一意に識別する文字列)、第二引数にはアプリケーションフラグ(アプリケーションの挙動を指定するフラグ)を受け取り、`Result<gtk::Application, glib::error::BoolError>`を返すので、パターンマッチでその値を取り出しています。`Ok`のケースで、実際にアプリケーションを実行しています。

流れとしては、最初に`app.connect_activate()`で`GtkApplication`へactivateシグナルに対するハンドラを定義し、最後に`app.run()`でアプリケーションの実行を開始しています。

GTK+のオブジェクトの多くはシグナルを受け取ることができ、また、シグナルを受け取った時に実行するハンドラを定義することができます。

今回の場合は、`app.run()`を実行すると、`app`に対してactivateシグナルを送信されます。`GtkApplication`はactivateシグナルの受信をもってアプリケーションのメイン処理を開始することになるので、`app.connect_activate()`の引数にactivateシグナルに対するハンドラとしてメイン処理を含むクロージャを渡しています。

ハンドラの引数には、activateシグナルを受信した`gtk::Application`のインスタンスが渡されます。

メイン処理(クロージャの中)では、`gtk::ApplicationWindow::new()`で、アプリケーションのウィンドウを表す`GtkApplicationWindow`オブジェクトを生成し、`win.set_title()`でウィンドウのタイトルバーに表示する文字列を設定しています。

ウィンドウのオブジェクトは生成しただけでは画面には表示されない為、最後に`win.show_all()`でオブジェクトを可視状態にしています。

説明を飛ばしましたが、冒頭でいくつかトレイトをスコープに取り込んでいます。`gtk::WindowExt`は、`set_title()`メソッドの為に、`gtk::WidgetExt`は`show_all()`メソッドの為に、そして`gio::ApplicationExt`は`run()`メソッドの為に取り込んでいます。

Gtk-rsでは、構造体「\*\*\*」に対するメソッドは、「\*\*\*Ext」という名前のトレイトに定義されています。

例えば、gtk::Window構造体に対するメソッドは、gtk::WindowExtトレイトに定義されています。
(但し、gtk::Applicationに対するメソッドは、gtk::GtkApplicationExtトレイトに定義されています。)

`gtk::Application`は、`gio::ApplicationExt`トレイトも実装しており、その`run()`メソッドを呼び出しています。

# エディタ領域を作成する
次に、エディタの編集領域を実装します。

GTK+では、GUIを構成する要素をWidgetと呼びます。例えば、先ほど作成したウィンドウ`gtk::ApplicationWindow`はWidgetです。また、ウィンドウ内に配置するボタン`gtk::Button`や、文字列を表示する`gtk::Label`等もWidgetです。

アプリケーションのGUIを構成するには、ウィンドウを起点に、その子要素として各種Widgetを追加していくことで実現します。例えば、ウィンドウ内にボタンを１つ設置したい場合は、`gtk::ApplicationWindow`に子要素として`gtk::Button`を追加します。

今回はウィンドウ内にテキスト編集領域を設置したいので、テキスト編集領域のオブジェクトとして`gtk::TextView`を`gtk::ApplicationWindow`の子要素として追加すれば良いように思います。しかし、このオブジェクトだけでは、ウィンドウのスクロール機能がありませんので、`gtk::ScrolledWindow`オブジェクトに包んでウィンドウの子要素に追加します。
Widgetの親子関係は以下になります。

```
gtk::ApplicationWindow
└── gtk::ScrolledWindow
    └── gtk::TextView
```

これを実装したコードは以下になります。(以降、変更部分のみ抜粋)

```rust:main.rs
...
// ContainerExtトレイトを使用するので取り込んでおく
use gtk::{ WidgetExt, WindowExt, ContainerExt };
...
...
        Ok(app) => {
            app.connect_activate(|app| {
                let win = gtk::ApplicationWindow::new(&app);
                //ウィンドウのデフォルトサイズを幅800ピクセル、高さ600ピクセルに指定
                win.set_default_size(800, 600);
                win.set_title("Vanilla Text"); 
                //スクロール可能な領域を生成
                let scr_win = gtk::ScrolledWindow::new(None, None);
                //テキスト編集領域を生成
                let txt_view = gtk::TextView::new();
                //TextViewをScrolledWindowの子要素として追加
                scr_win.add(&txt_view);
                //ScrolledWindowをApplicationWindowの子要素として追加
                win.add(&scr_win);
                win.show_all();
            });

            app.run(&[""]);
        },
...
```

`cargo run`を実行して以下のようにテキストの入力やウィンドウのスクロールができることを確認します。
![PrtSc_TextView.png](https://qiita-image-store.s3.amazonaws.com/0/55971/586b6d8a-bee6-f015-fd90-1d3d609ebd17.png)



# 編集コマンドを実装する
TextViewを設置しただけではコピペ等の編集コマンドは使えませんので、これらの機能を実装していきます。

まずは、コピーとペーストを実装します。
メニューバーから[Edit]-[Copy]、[Edit]-[Paste]のように選択して、コピー・ペーストができるようにします。
各メニューアイテムを選択した時に、コピーやペーストを実行させるシグナルをウィンドウに送信する必要があります。

ここで、シグナルとハンドラを紐付ける`Action`インターフェイス(今回はその具象クラスの`SimpleAction`)を使います。`SimpleAction`オブジェクトへ、activateシグナルに対するハンドラを紐付け、ウィンドウにこの`SimpleAction`を設定します。

```rust:main.rs
...
//クリップボードを扱う為GDKを使用
extern crate gdk;
...
//必要なトレイトを追加
use gtk::{ ...
           TextViewExt, TextBufferExt
};

use gio::{ ...
           SimpleActionExt, ActionMapExt
};

...
        Ok(app) => {
            app.connect_activate(|app| {
                ...
                //CopyコマンドのActionをアクション名"copy"として生成
                let copy_action = gio::SimpleAction::new("copy", None);
                {
                    let txt_view = txt_view.clone();
                    //activateシグナルに対するハンドラを定義
                    copy_action.connect_activate(move |_, _| {
                        //アプリケーションのデフォルトクリップボードを取得
                        let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
                        //TextViewのTextBufferから現在選択している部分の内容をクリップボードにコピー
                        txt_view.get_buffer().unwrap().copy_clipboard(&clipboard);
                    });
                }

                //PasteコマンドのActionをアクション名"paste"として生成
                let paste_action = gio::SimpleAction::new("paste", None);
                {
                    let txt_view = txt_view.clone();
                    paste_action.connect_activate(move |_, _| {
                        let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
                        let buf = txt_view.get_buffer().unwrap();
                        //TextBufferのカーソル位置又は選択部分にクリップボードの内容をペースト
                        buf.paste_clipboard(&clipboard, None, txt_view.get_editable());
                    });
                }

                //ウィンドウにActionを設定
                win.add_action(&copy_action);
                win.add_action(&paste_action);
                ...
```

GDKを使うので、Cargo.tomlの`[dependencies]`に`gdk = "0.6.0"`を追加しておいて下さい。

activateシグナルに対するハンドラ内では、先ほど生成したTextViewオブジェクトを使用するので、クロージャで`txt_view`をキャプチャします。このクロージャは`static`ライフタイムを持つことになるので、キャプチャする値は`move`します。その為、事前に`txt_view`のコピーを作成し、そのコピーをキャプチャしています。

Gtk-rsが提供するWidgetの`clone()`によるコピーは、内部的にはオブジェクトへのポインターのコピーなのでborrowチェックをパスし安全に扱えます。(コピーのコストも低い)

これで、ウィンドウに対して２つのアクションを設定できました。

次に、メニューバーからコピー・ペーストを実行できるようにメニューバーを作成します。
メニューバーは、サブメニューとして[Edit]を持ち、[Edit]サブメニューはメニューアイテムとして[Copy]と[Paste]を持つ構成となります。各メニューアイテムにアクション名を指定する事で、選択時にそのアクションへactivateシグナルを送信します。

```rust:main.rs
...
use gtk::{ ...
           GtkApplicationExt
};

use gio::{ ...
           MenuExt
};
...
        Ok(app) => {
            app.connect_activate(|app| {
            ...
                //gio::Menu、MenuItemを一時的に使いたい為スコープを作っておく(必須ではない)
                {
                    use gio::{Menu, MenuItem};
                    
                    //メニューバーを表すオブジェクトを生成
                    let menubar = Menu::new();
                    //Editサブメニューを表すオブジェクトを生成
                    let submenu_edit = Menu::new();
                    //メニュー表示文字列"Copy"、対応するアクション名"win.copy"でメニューアイテム作成
                    let copy = MenuItem::new("Copy", "win.copy");
                    //同様に"Paste"のメニューアイテム作成
                    let paste = MenuItem::new("Paste", "win.paste");

                    //Editサブメニューにメニューアイテムを設定
                    submenu_edit.append_item(&copy);
                    submenu_edit.append_item(&paste);

                    //メニューバーにEditサブメニューを設定
                    menubar.append_submenu("Edit", &submenu_edit);

                    //アプリケーションにメニューバーを設定
                    app.set_menubar(&menubar);
                }
...
```

`Action`は`ActionGroup`を使って特定のグループに分類することができます。`ActionGroup`は、その名前とそのグループに属する`Action`で構成されます。
`GtkApplicationWindow`オブジェクトは内部に`ActionGroup`を持っており、その`ActionGroup`の名前は`"win"`として登録されます。

`MenuItem::new()`で指定するアクションは`"<プレフィクス>.<アクション>"`という形式で指定しますがこの<プレフィクス>には`ActionGroup`の名前を指定し、<アクション>に`Action`オブジェクトのアクション名を指定します。
(よってコピーのアクションは`"win.copy"`)

こんな感じでメニューバーができました。
![PrtSc_copy_paste.png](https://qiita-image-store.s3.amazonaws.com/0/55971/6c07ddba-4810-7897-0dec-d7da86f5ad05.png)

# 新規ウィンドウ作成
メニューバーから[File]-[New window]を選択して、新規にウィンドウを作成できるようにします。

新規に作成されるウィンドウはアプリケーション起動時のウィンドウと同じまっさらなウィンドウにするのでウィンドウのUI作成とアクション設定のコードを`create_window()`関数として再利用できるようにします。
また、ついでに[File]-[Quit]でアプリケーションを終了できるようにしておきます。

以上の変更を加えたここまでの全コードをは以下になります。

```rust:main.rs
extern crate gtk;
extern crate gio;
extern crate gdk;

use gtk::{ WidgetExt, WindowExt, ContainerExt,
           GtkApplicationExt, TextViewExt, TextBufferExt            
};

use gio::{ ApplicationExt, MenuExt, SimpleActionExt,
           ActionMapExt
};

fn main() {
    match gtk::Application::new("com.github.koji-m.vanilla_text", gio::APPLICATION_HANDLES_OPEN) {
        Ok(app) => {
            app.connect_activate(|app| {
                let new_window_action = gio::SimpleAction::new("new_window", None);
                {
                    let app = app.clone();
                    new_window_action.connect_activate(move |_, _| {
                        create_window(&app);
                    });
                }

                let quit_action = gio::SimpleAction::new("quit", None);
                {
                    let app = app.clone();
                    quit_action.connect_activate(move |_, _| {
                        app.quit();
                    });
                }

                app.add_action(&new_window_action);
                app.add_action(&quit_action);

                {
                    use gio::{Menu, MenuItem};
                    
                    let menubar = Menu::new();

                    let submenu_file = Menu::new();
                    let newwindow = MenuItem::new("New Window", "app.new_window");
                    let quit = MenuItem::new("Quit", "app.quit");

                    submenu_file.append_item(&newwindow);
                    submenu_file.append_item(&quit);

                    let submenu_edit = Menu::new();
                    let copy = MenuItem::new("Copy", "win.copy");
                    let paste = MenuItem::new("Paste", "win.paste");

                    submenu_edit.append_item(&copy);
                    submenu_edit.append_item(&paste);

                    menubar.append_submenu("File", &submenu_file);
                    menubar.append_submenu("Edit", &submenu_edit);

                    app.set_menubar(&menubar);
                }


                create_window(&app);

            });

            app.run(&[""]);
        },
        Err(_) => {
            println!("Application start up error");
        }
    };
}

fn create_window(app: &gtk::Application) -> gtk::ApplicationWindow {
    let win = gtk::ApplicationWindow::new(app);
    win.set_default_size(800, 600);
    win.set_title("Vanilla Text");
    let scr_win = gtk::ScrolledWindow::new(None, None);
    let txt_view = gtk::TextView::new();
    scr_win.add(&txt_view);
    win.add(&scr_win);

    let copy_action = gio::SimpleAction::new("copy", None);
    {
        let txt_view = txt_view.clone();
        copy_action.connect_activate(move |_, _| {
            let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
            txt_view.get_buffer().unwrap().copy_clipboard(&clipboard);
        });
    }

    let paste_action = gio::SimpleAction::new("paste", None);
    {
        let txt_view = txt_view.clone();
        paste_action.connect_activate(move |_, _| {
            let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
            let buf = txt_view.get_buffer().unwrap();
            buf.paste_clipboard(&clipboard, None, txt_view.get_editable());
        });
    }

    win.add_action(&copy_action);
    win.add_action(&paste_action);

    win.show_all();

    win
}
```

# ファイルを編集・保存できるようにする
まだファイルを開いて編集して保存という重要な機能がありませんのでここで実装していきます。

まずは、ファイルを開く機能ですが、メニューバーから[File]-[Open]を選択するとファイル選択のダイアログウィンドウが表示され、そこで開くファイルを選択するという、よくある流れを実装します。

```rust:main.rs
use gtk::{ ...
           DialogExt, FileChooserExt, BinExt, Cast
};

use gio::{ ...
           FileExt
};

...

        Ok(app) => {
            app.connect_activate(|app| {
            ...
                //Openコマンドアクションを生成
                let open_action = gio::SimpleAction::new("open", None);
                {
                    let app = app.clone();
                    //ファイル選択ダイアログを起動して選択されたファイルの内容をウィンドウ内に表示
                    open_action.connect_activate(move |_, _| {
                        if let Some(file) = run_file_chooser_dialog() {
                            open(file, app.clone());
                        }
                    });
                }
            ...
                app.add_action(&open_action);
            ...
                {
                 ...
                    let open = MenuItem::new("Open", "app.open");
                    ...
                    submenu_file.append_item(&open);

...

//ファイル選択ダイアログ表示し、選択されたファイルをGFileオブジェクトとして返す
fn run_file_chooser_dialog() -> Option<gio::File> {
    //GtkFileChooserDialogオブジェクトを生成
    let dialog = gtk::FileChooserDialog::new::<gtk::Window>(Some("Open File"),
                                     None,
                                     gtk::FileChooserAction::Open);
    //ファイル選択ダイアログに"Open"ボタンと"Cancel"ボタンを追加
    dialog.add_button("Cancel", gtk::ResponseType::Cancel.into());
    dialog.add_button("Open", gtk::ResponseType::Accept.into());

    let file;
    //ファイル選択ダイアログを表示しユーザの入力を受け取る
    if dialog.run() == gtk::ResponseType::Accept.into() {
        //ユーザが"Open"ボタンをクリックした場合は選択したファイルのパスを取得する
        if let Some(path) = dialog.get_filename() {
            //ファイルのパスからGFileオブジェクトを生成する
            file = Some(gio::File::new_for_path(path.as_path()))
        } else {
            file = None
        }
    } else {
        file = None
    }

    //ファイル選択ダイアログを閉じる
    dialog.destroy();

    file
}

//ファイルを開いてウィンドウのタイトルバーにファイル名を設定する
fn open(file: gio::File, app: gtk::Application) {
    //新規にウィンドウを生成
    let win = create_window(&app);

    //ファイル読み込み
    load_file(file.clone(), win.clone());
    //ウィンドウのタイトルバーにファイル名を設定
    win.set_title(file.get_basename().unwrap().to_str().unwrap());
}

//ファイルをウィンドウのTextViewに読み込む
fn load_file(file: gio::File, win: gtk::ApplicationWindow) {
    //GFileオブジェクトからファイルの内容をファイルの内容(型はVec<u8>)を読み込む
    if let Ok((v, _)) = file.load_contents(None) {
        //Vec<u8>からStringへ変換
        let text = String::from_utf8(v).unwrap();

        //winから子要素を辿ってTextBufferを取り出す
        let scr_win = win.get_child().unwrap().downcast::<gtk::ScrolledWindow>().ok().unwrap();
        let txt_view = scr_win.get_child().unwrap().downcast::<gtk::TextView>().ok().unwrap();
        let buf = txt_view.get_buffer().unwrap();

        //TextBufferにファイルの内容(型はString)を読み込む
        buf.set_text(&text);
    }

}

```

ファイル選択ダイアログは`GtkFileChooserDialog`オブジェクトになります。このオブジェクトにボタンを設置し、各ボタンに対応する`GtkResponseType`によってファイルを読み込むのか何もしないのかを判断します。`GtkFileChooserDialog`は、`run()`メソッドでダイアログウィンドウを起動し、ユーザがボタンをクリックすることで`GtkResponseType`を返します。この時点で、`GtkFileChooserDialog`オブジェクトにはユーザが選択したファイルのパスを内部で保持しているので、`get_filename()`メソッドでそれを取得します。

尚、`GtkFileChooserDialog`は、ユーザがボタンをクリックしただけでは閉じないので、結果を受け取った後`destroy()`メソッドで閉じます。

実際にウィンドウ内の`GtkTextView`にファイルの内容を読み込むには、`GtkTextView`内の`GtkTextBuffer`に内容を読み込みます。

次に、ウィンドウ内のテキストをファイルに保存する機能を実装します。

ファイルへの保存は、`GtkTextBuffer`の内容をファイルに書き込めば良いのですが、現状その`GtkTextBuffer`の内容に紐付くファイルを特定する方法がありません。つまり、ウィンドウに現在編集中のファイルが紐付いていませんので、これらを紐付けるデータ構造を新規に作ります。

GTK+はクラスとその継承を利用してプログラムを作成できるので、通常今回のようなケースでは`GtkApplicationWindow`クラスを継承するクラスを作成して、そのクラスにファイル名を保持するメンバー変数を定義すればスマートに実装できます。

しかし、Rustは構造体とトレイトを組み合わせてプログラムを作成するので、既存の`gtk::ApplicationWindow`構造体に直にフィールドを追加することはできません。

その為今回は、新たに`WindowCore`という構造体を定義して、`gtk::ApplicationWindow`構造体とファイル情報(`gio::File`構造体)をフィールドに持たせます。

ついでに、ウィンドウ内のテキストの変更を保存せずにウィンドウを閉じようとした場合、ダイアログを出して[変更を破棄する]か[保存する]もしくは[キャンセル]するかをユーザに確認するようにしたいので、`GtkTextBuffer`の内容が変更されているかについても`WindowCore`構造体にステータスとして保存しておくようにします。

以下が`WindowCore`構造体の定義になります。

```rust
struct WindowCore {
    window: gtk::ApplicationWindow,  //GtkApplicationWindow本体
    file: Option<gio::File>,         //このウィンドウに紐付くファイルの情報
    changed: bool,                   //ウィンドウ内の内容が変更されているかの状態
}
```

この`WindowCore`構造体を使用して、Saveアクションを実装します。

具体的には、`WindowCore`の`file`に設定されているファイルに`GtkTextBuffer`の内容を書き出し、`changed`フィールドに`false`を設定します。

この処理は以下のように書きたいのですが、これではうまくいきません。

```rust
let save_action = gio::SimpleAction::new("save", None);
{
    //Saveアクションのハンドラ内でwinのchangedフィールドの値を設定したいので
    //winの複製ではなく、winへの参照をクロージャにキャプチャさせたいがlifetimeの制約によりNG。
    let win = &mut win;
    save_action.connect_activate(move |_, _| {
        win.save_file();
        win.changed = false;
    });
}
```

上記コード内のコメントの通り、`WindowCore`構造体への参照をstatic lifetimeのクロージャにキャプチャさせることはできない為[^3]、別の方法で参照をキャプチャさせます。

[^3]: コンパイラからすると実体より、キャプチャした実体への参照の方が長く生き続けるように見えるのでNG。

今回その方法として、スマートポインタ`std::rc::Rc<T>`を使います。`Rc<T>`はヒープ領域上の`T`への参照のようなものを実現する構造体で、`clone()`メソッドでその参照を複製できます。また、ダングリングポインタやメモリリークが発生しないよう`T`への参照数を管理しています。(`Rc`はReference Countedの略)

つまり、`Rc<WindowCore>`を作って、これを`clone()`してクロージャにキャプチャさせたいのですが、まだこれでもうまくいきません。`WindowCore`構造体が複数箇所から参照されることになると、Rustのルールにより`WindowCore`自体への変更ができなくなります。[^4]

[^4]: WindowCoreに対するimmutable borrowが発生している状態で、mutable borrowはできない。

なので、さらに`std::cell:RefCell`でラッピングして、`Rc<RefCell<WindowCore>>`として扱います。
`RefCell`は、`borrow()`と`borrow_mut()`メソッドを使うことでそれぞれimmutable borrowとmutable borrowを実行します。

以上の方法により、`Rc<RefCell<WindowCore>>`を`clone()`したものをクロージャにキャプチャして、クロージャ内では
その構造体に対して`borrow_mut()`で`WindowCore`をmutable borrowして変更を加えます。

`RefCell`は実行時にRustのborrowルールに則ってborrowチェックをするので、既に`borrow()`されている状態で`borrow_mut()`を実行するとランタイムパニックとなります。
(`Rc`や`RefCell`はコンパイル時にチェックすることを実行時にチェックしているので、その分実行時のコストが増えます。)

ウィンドウを`Rc<RefCell<WindowCore>>`として扱うように変更し、これを`Window`型として別名を付けて別ファイルwin.rsに纏めたのが以下になります。

```rust:win.rs
extern crate gtk;
extern crate gio;
extern crate gdk;

use gtk::{
    ResponseType, TextViewExt,
    TextBufferExt,
    WidgetExt, FileChooserExt, DialogExt,
    ContainerExt,
    BinExt, Cast, WindowExt,
};

use gio::{
    FileExt, SimpleActionExt, ActionMapExt
};

use std::cell::RefCell;
use std::rc::Rc;
use std::path::Path;


pub struct WindowCore {
    window: gtk::ApplicationWindow,
    file: Option<gio::File>,
    text_view: gtk::TextView,
    changed: bool,
}

pub type Window = Rc<RefCell<WindowCore>>;

pub trait WindowExtend {
    fn create(app: &gtk::Application) -> Window;
    fn init(&self);
    fn load_file(&self, file: gio::File);
    fn save_file(&self);
    fn save_buffer(&self);
    fn save_as(&self);
    fn save_file_chooser_run(&self) -> Option<gio::File>;
    fn window(&self) -> gtk::ApplicationWindow;
    fn file(&self) -> Option<gio::File>;
    fn set_file(&self, file: Option<gio::File>);
    fn text_view(&self) -> gtk::TextView;
    fn set_changed(&self, changed: bool);
    fn set_title(&self, title: &str);
}

impl WindowExtend for Window {
    fn create(app: &gtk::Application) -> Window {
        let window = gtk::ApplicationWindow::new(app);
        window.set_default_size(800, 600);
        window.set_title("Vanilla Text");
        let scr_win = gtk::ScrolledWindow::new(None, None);
        let txt_view = gtk::TextView::new();
        scr_win.add(&txt_view);
        window.add(&scr_win);

        let win = Rc::new(RefCell::new(
                WindowCore {window: window.clone(),
                            file: None,
                            text_view: txt_view.clone(),
                            changed: false}));

        let copy_action = gio::SimpleAction::new("copy", None);
        {
            let txt_view = txt_view.clone();
            copy_action.connect_activate(move |_, _| {
                let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
                txt_view.get_buffer().unwrap().copy_clipboard(&clipboard);
            });
        }

        let paste_action = gio::SimpleAction::new("paste", None);
        {
            let txt_view = txt_view.clone();
            paste_action.connect_activate(move |_, _| {
                let clipboard = txt_view.get_clipboard(&gdk::SELECTION_CLIPBOARD);
                let buf = txt_view.get_buffer().unwrap();
                buf.paste_clipboard(&clipboard, None, txt_view.get_editable());
            });
        }

        let save_action = gio::SimpleAction::new("save", None);
        {
            let win = win.clone();
            save_action.connect_activate(move |_, _| {
                win.save_file();
            });
        }

        window.add_action(&copy_action);
        window.add_action(&paste_action);
        window.add_action(&save_action);

        window.show_all();

        win
    }


    fn init(&self) {
        self.borrow().window.show_all();
    }

    fn load_file(&self, file: gio::File) {
        if let Ok((v, _)) = file.load_contents(None) {
            let text = String::from_utf8(v).unwrap();

            let scr_win = self.window().get_child().unwrap().downcast::<gtk::ScrolledWindow>().ok().unwrap();
            let txt_view = scr_win.get_child().unwrap().downcast::<gtk::TextView>().ok().unwrap();
            let buf = txt_view.get_buffer().unwrap();
            buf.set_text(&text);
        }

    }

    fn save_file(&self) {
        if self.file().is_some() {
            self.save_buffer();
        } else {
            self.save_as()
        }
    }

    fn save_buffer(&self) {
        if let Some(buf) = self.text_view().get_buffer() {
            let (start, end) = buf.get_bounds();
            if let Some(text) = buf.get_text(&start, &end, true) {
                if self.file().as_ref().unwrap().replace_contents(text.as_bytes(),
                                         None,
                                         true,
                                         gio::FILE_CREATE_NONE,
                                         None).is_ok() {
                    self.set_changed(false);
                } else {
                    let dialog = gtk::MessageDialog::new(Some(&self.window()),
                                                         gtk::DIALOG_MODAL,
                                                         gtk::MessageType::Error,
                                                         gtk::ButtonsType::Close,
                                                         "Error: Cannot save file");
                    dialog.run();
                    dialog.destroy();
                }
            }
        }
    }

    fn save_as(&self) {
        if let Some(file) = self.save_file_chooser_run() {
            self.set_title(file.get_basename().unwrap().to_str().unwrap());
            self.set_file(Some(file));
            self.save_buffer();
        }
    }

    fn save_file_chooser_run(&self) -> Option<gio::File> {
        let dialog = gtk::FileChooserDialog::new::<gtk::Window>(Some("Save File"),
                                                                Some(&self.window().upcast()),
                                                                gtk::FileChooserAction::Save);
        dialog.add_button("Cancel", ResponseType::Cancel.into());
        dialog.add_button("Save", ResponseType::Accept.into());
        dialog.set_do_overwrite_confirmation(true);

        if let Some(ref f) = self.file() {
            dialog.set_filename(f.get_path().unwrap().as_path());
        } else {
            dialog.set_current_name(Path::new("untitled"));
        }

        let file;
        if dialog.run() == ResponseType::Accept.into() {
            if let Some(path) = dialog.get_filename() {
                file = Some(gio::File::new_for_path(path.as_path()))
            } else {
                file = None
            }
        } else {
            file = None
        }

        dialog.destroy();

        file
    }

    fn window(&self) -> gtk::ApplicationWindow {
        self.borrow().window.clone()
    }

    fn file(&self) -> Option<gio::File> {
        self.borrow().file.clone()
    }

    fn set_file(&self, file: Option<gio::File>) {
        self.borrow_mut().file = file;
    }

    fn set_changed(&self, changed: bool) {
        self.borrow_mut().changed = changed;
    }

    fn text_view(&self) -> gtk::TextView {
        self.borrow().text_view.clone()
    }

    fn set_title(&self, title: &str) {
        self.borrow().window.set_title(title);
    }
}
```

長くなるので、main.rsの修正版はgist([main.rs](https://gist.github.com/koji-m/b88a887543f2ffd0b9ce689c532ee466))に貼っておきます。

これで、ファイルへの保存ができるようになりましたが、まだ以下の機能がありません。

* ウィンドウを閉じる際、内容が変更されている場合は保存を促す。
* ファイルを開く際、すでに別のウィンドウでそのファイルを編集中の場合は新規ウィンドウを開かない。
* ファイルを開く際、未編集のウィンドウがある場合はそのウィンドウでファイルを開く。

最後の２つを実装するには、現在開いている全ウィンドウを管理する必要があります。
(`Window`のリストをアプリケーション内で保持しておく必要があります。)

現在開いている全`Window`を`Vec`に保持しておいて、今開こうとしているファイルを保持している`Window`がないか、また、未編集の`Window`がないかをこの`Vec`から探して適切な処理をします。

これを実現にするには、下記のようにOpenアクションのハンドラ内のどこかで`Vec`へ`Window`を追加する必要があります。

```rust:
let wins: Vec<Window>;
...
{
    let win = win.clone();
    open_action.connect_activate(move |_, _| {
        ...
        wins.push(win);
        ...
    });
}
```

ここでも、`Vec`への参照をクロージャにキャプチャさせ、かつ`Vec`への追加・削除ができるようにする為、以下のように`Windows`型を導入します。

```rust:
type Windows = Rc<RefCell<Vec<Window>>>;
```

以上の追加と以下追加を含めたコードをgistに貼っておきます。([main.rs](https://gist.github.com/koji-m/8f19159bbcab3b1fa3704972375d08f0)、[win.rs](https://gist.github.com/koji-m/5fb31be2cae0e482d048341e12c6f5a5))

* Ctrl-c などのショートカットキー(accelerator)を設定
* メニューをいくつか追加、[about]ではダイアログ内にロゴ画像(自前で用意したassets/logo.png)を表示。
* コマンドライン引数にファイルを指定できるように、アプリケーションにopenシグナルハンドラを設定(`app.connect_open()`)
* アプリケーションのactivateやopenシグナルより前に発生するstartupシグナルに対するハンドラ内で、アクションやUIを設定(`app.connect_startup()`)

ここまでで、タブ無しのテキストエディタができました。`cargo run -- <file name>...`と実行すると、起動時に(複数の)ファイルを開くこともできます。

# タブを導入
最後に、ここまで作ってきたテキストエディタにタブを導入します。

方法はいくつかあるかと思いますが、今回は単純に`GtkScrolledWindow`をタブのボディ部分(ページ内)に持たせます。

### タブ型UIのWidgetの構成

タブは`GtkNotebook`クラスで実現します。
`GtkNotebook`クラスは以下のような構造をしており、１つの`GtkNotebook`に複数の`GtkNotebookPage`を保持させることができ、各`GtkNotebookPage`はコンテンツ(child)とそれに紐付くタブ(tab_label)で構成されます。

```
GtkNotebook
├── GtkNotebookPage
│   ├── child
│   └── tab_label
├── GtkNotebookPage
│   ├── child
│   └── tab_label
.
.
```

今回はタブに、開いているファイルのファイル名と閉じるボタンを設置し、コンテンツにはテキスト編集領域を設置したいのでオブジェクトの配置は下記のようにします。

```
GtkApplicationWindow
└── GtkNotebook
    ├── GtkNotebookPage
    │   ├── GtkBox (child)
    │   │   ├── GtkRevealer
    │   │   │   └── GtkInfoBar
    │   │   │       └── GtkBox
    │   │   │           └── GtkLabel
    │   │   └── GtkScrolledWindow
    │   │       └── GtkTextView
    │   └── GtkBox (tab_label)
    │       ├── GtkLabel
    │       └── GtkButton
    ├── GtkNotebookPage
    .
    .
```

コンテンツ(child)内の`GtkRevealer`以下のオブジェクトは、ウィンドウ内の上部に表示するインフォメーションバーになります。

ファイルを開くときに既にそのファイルが別のタブで編集中の場合、その旨を知らせるWarningメッセージを表示するのに使います。

メッセージ表示は`GtkInfoBar`で行っていますが、そのメッセージ自体を表示したり隠したりをコントロールする為に`GtkRevealer`を使っています。

### タブを管理するデータ構造

これまではウィンドウ(`WindowCore`)で、編集しているファイルやテキストが変更されているかを管理してきましたが、ここからはタブ(ページ)で管理していく必要がありますので、新たにデータ構造を導入します。

```rust:
pub struct PageCore {
    tab: gtk::Box,
    tab_label: gtk::Label,
    revealer: gtk::Revealer,
    contents: gtk::Box,
    text_view: gtk::TextView,
    close_button: gtk::Button,
    file: Option<gio::File>,
    changed: bool,
}
```

先ほどの`GtkNotebookPage`内の各部品に直接アクセスできるようにフィールドを持たせています。

更に`Window`、`Windows`と同様に、`PageCore`を直接使わずに`Page`と`Pages`を使うようにします。

```rust:
pub type Page = Rc<RefCell<PageCore>>;
pub type Pages = Rc<RefCell<Vec<Page>>>;
```

以上、UIのオブジェクト構成とタブを管理するデータ構造となります。

これらを扱う関数も実装していきますが、扱う対象を`Window`から`Page`に変えて少し修正するだけなので難しいところはありません。

ここまでで完成となりますが、一部コードを改善したいところがありますので最後の修正をしていきます。

### UIをXMLで定義

このアプリではUIの構成はシンプルなので気になりませんが、より複雑なUIを構成する場合、UIオブジェクトを構成するコードの見通しが悪くなり、画面のイメージもつきにくくなります。

GTK+ではUIの構成をXMLで記述することができるので、UIを構成するコードを個別のXMLファイルとして切り出していきます。

以下ウィンドウの構造を定義したXMLになります。

```xml:window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkApplicationWindow" id="window">
    <property name="default-width">800</property>
    <property name="default-height">600</property>
    <property name="title">~new file~</property>
    <child>
      <object class="GtkNotebook" id="notebook">
      </object>
    </child>
  </object>
</interface>
```

コード内でこのXMLファイルを読み込んで`GtkBuilder`オブジェクトを生成し、`id`属性の文字列を指定してオブジェクトを取得します。

さらに、アプリケーションのメニューもXMLに切り出します。こちらもコード内では`GtkBuilder`オブジェクトとして扱えます。

```xml:menu.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <menu id="app_menu">
    <section>
      <item>
        <attribute name="label">New Window</attribute>
        <attribute name="action">app.new_window</attribute>
      </item>
      <item>
        <attribute name="label">Quit</attribute>
        <attribute name="action">app.quit</attribute>
      </item>
    </section>
  </menu>
  <menu id="menu_bar">
    <submenu>
      <attribute name="label">File</attribute>
      <section>
        <item>
          <attribute name="label">New Tab</attribute>
          <attribute name="action">win.new_tab</attribute>
        </item>
      </section>
      <section>
        <item>
          <attribute name="label">Save</attribute>
          <attribute name="action">win.save</attribute>
        </item>
        <item>
          <attribute name="label">Save as...</attribute>
          <attribute name="action">win.saveas</attribute>
        </item>
      </section>
      .
      .
```

今回作成したアプリケーションはGitHub([VanillaText](https://github.com/koji-m/VanillaText))にあげておきます。

# おまけ
GTK+とあまり関係ないですが、アプリケーションを配布する際に役立つ情報も簡単に纏めておきます。

### メニューバーにアプリケーション名を表示する
作成したアプリをUbuntuで実行するとメニューバーに「名前なし」と表示されます。
![PrtScr_no_app_name.png](https://qiita-image-store.s3.amazonaws.com/0/55971/146e32de-b844-436a-10b2-d09d42e354a1.png)

通常、配布されているアプリではここにアプリケーション名が表示されます。(Chromeの場合、「Google Chrome」と表示される。)
アプリケーション名を表示するには、`/usr/share/applications/`に<アプリケーション名>.desktopというファイルを設置します。

.desktopファイルには以下のように、`Name=Vanilla Text`と指定することでアプリケーション名が表示されます。

```ini:vanilla_text.desktop
[Desktop Entry]
Type=Application
Encoding=UTF-8
Name=Vanilla Text
Comment=A simple text editor
Exec=/usr/bin/vanilla_text
Icon=vanilla_text
Terminal=false
```

![PrtSc_app_name.png](https://qiita-image-store.s3.amazonaws.com/0/55971/fd99fb6e-6406-faff-01f8-faa76b558b90.png)


### アイコンを設定する
デスクトップのランチャーに表示されるアイコンやアプリケーション切り替え時のアイコンも「?」アイコンのままなので設定します。

アイコンはそのサイズごとに特定のディレクトリに保存することで表示されるようになります。
保存場所は複数ありますが、今回は`/usr/share/icons/hicolor/`配下に保存します。

今回用意するアイコンのサイズは、ランチャー表示用に48x48ピクセルとDash検索表示用に64x64ピクセル、アプリ切り替え表示用に128x128ピクセルのPNGファイルとします。

それぞれ、上記ディレクトリ配下のサイズごとにわかれたディレクトリに<実行ファイル名>.pngというファイル名で保存します。

例えば、64x64ピクセルのPNGファイルは`/usr/share/icons/hicolor/64x64/apps/vanilla_text.png`に保存しています。

尚、アイコンをシステムに認識させるには、画像ファイルを保存した後、以下コマンドを実行してアイコンのキャッシュを更新する必要があります。

```bash
~$ gtk-update-icon-cache /usr/share/icons/hicolor
```

ランチャーにアイコンが表示されます。

![PrtSc_launcher_icon.png](https://qiita-image-store.s3.amazonaws.com/0/55971/59d70ec4-b108-abc4-c948-32da69ff583a.png)

※アイコンの作り方に興味がある方は以下ご参考まで。
[Gravit Designerを使って３分でアイコンを作る](https://www.slideshare.net/kojimatsumoto10/gravit-designer-82898998)

### Makefileを作る
最後にCargoからではなく、コマンドとして実行できるようにビルドしてインストールします。

UIを記述したXMLファイルや.desktopファイル、アイコンファイルなども適切な場所にインストールする必要があるので以下のようなMakefileを作成します。

```make:Makefile
all:
	cargo build --release

install:
	install -Dm 755 target/release/vanilla_text /usr/bin/vanilla_text
	mkdir -p /usr/share/vanilla_text/ui
	install -Dm 755 ui/* /usr/share/vanilla_text/ui/
	install -Dm 644 assets/vanilla_text.desktop /usr/share/applications/vanilla_text.desktop
	install -Dm 644 assets/icon_48x48.png /usr/share/icons/hicolor/48x48/apps/vanilla_text.png
	install -Dm 644 assets/icon_64x64.png /usr/share/icons/hicolor/64x64/apps/vanilla_text.png
	install -Dm 644 assets/icon_128x128.png /usr/share/icons/hicolor/128x128/apps/vanilla_text.png
	gtk-update-icon-cache /usr/share/icons/hicolor

uninstall:
	rm /usr/bin/vanilla_text
	rm -r /usr/share/vanilla_text
	rm /usr/share/applications/vanilla_text.desktop
	rm /usr/share/icons/hicolor/48x48/apps/vanilla_text.png
	rm /usr/share/icons/hicolor/64x64/apps/vanilla_text.png
	rm /usr/share/icons/hicolor/128x128/apps/vanilla_text.png
	gtk-update-icon-cache /usr/share/icons/hicolor

```

# 最後に
以上でGTK+とGtk-rsを使ったテキストエディタが完成しました。

コードはGitHub([VanillaText](https://github.com/koji-m/VanillaText))にあげておきます。

また、参考にしたサイトやコードを挙げておきます。

* [Getting Started with GTK+](https://developer.gnome.org/gtk3/stable/gtk-getting-started.html) (まずはここから)
* [GNOME DEVELOPER API Reference](https://developer.gnome.org/references) (APIの使い方はこちら)
* [GTK+のソースコード](https://github.com/GNOME/gtk) (リファレンスだけではわからないときに参照)
* [Gtk-rs documentation] (http://gtk-rs.org/docs-src/) (Gtk-rsのAPIリファレンス)
* [Gtk-rsのソースコード](https://github.com/gtk-rs) (リファレンスだけではわからないときに参照)
* [geditのソースコード](https://github.com/GNOME/gedit) (テキストエディタの作り方の参考に)

GtkTreeViewを使ったGUIアプリの作り方についてはこちらをどうぞ。
[いまさらGTKとRustでLinuxデスクトップアプリ入門 -LANアナライザを作る-](https://qiita.com/koji_mats/items/4082f141c962b84342ef)