RustでGUI 〜conrod編〜

コード全体はgithubの[ogata-k/GUI_cmp/example_conrod](https://github.com/ogata-k/GUI_cmp/tree/master/example_conrod)を参考にしてください。

# conrod
今回は[こちら](http://mmi.hatenablog.com/entry/2017/07/09/234945)を参考にしてそこに[挙げられている例](https://github.com/mmisono/conrod-examples/tree/master/fibonacci)の指定の通りのバージョン```0.53.0```を使用しています。
## どんなクレートか
[conrod](https://github.com/PistonDevelopers/conrod)は[piston](https://www.piston.rs/)というゲームエンジンを開発しているところが作成しているRust純正のGUIライブラリです。```widget```の構成や管理、```widget```に対するイベントの伝播などの基本的な機能を担当できます。しかし、実際の描画やOSからのイベントの受取はconrodは、conrodに用意されている描画用の[glium](https://github.com/glium/glium)(OpenGL)、イベントの管理用の[winit](https://github.com/tomaka/winit)のバックエンドが用意されているので、そちらを使うことになります。
conrodで作られたGUIはRustらしくマルチプラットフォームで動作するので簡単にほかのOSに対応できます。

conrodは```widget```毎に```Id```を用意して管理します。複雑になればなるほど```Id```の用意が大変になって管理しづらくなってくるのですが、```Id```の一意性から```widget```の変数名みたいな扱いができる帯出すのが。これにより取得した```Id```によって```widget```を判断したり、```widget```を指定して呼び出すのが簡単にできるようになります。

具体例が[ここ](https://github.com/PistonDevelopers/conrod/tree/master/backends/conrod_glium/examples)にいくつか載っているので参考になると思います。


## コード
カウントアップをするプログラムを書いていきます。

``` rust:main.rs
#[macro_use]
extern crate conrod;
extern crate find_folder;

use conrod::{widget, color, Colorable, Borderable, Sizeable, Positionable, Labelable, Widget};
use conrod::backend::glium::glium;
use conrod::backend::glium::glium::{DisplayBuild, Surface};

// 使用するid一覧
widget_ids!(
    struct Ids {
        canvas,
        num_lbl,
        button,
    });

fn main() {
  // 設定値
    const TITLE: &'static str = "カウントアップ";
    let width = 400;
    let height = 300;

    // windowの作成
    let display = glium::glutin::WindowBuilder::new()
        .with_dimensions(width, height)
        .with_title(TITLE)
        .build_glium()  // windowの構築
        .unwrap();

    // Uiの作成
    let mut ui = conrod::UiBuilder::new([width as f64, height as f64]).build();

    // Uiで使うFontをassets以下のファイルからfont::Mapに追加
    let assets = find_folder::Search::KidsThenParents(3, 5)
        .for_folder("assets")
        .unwrap();
    let font_path = assets.join("fonts/NotoSans/NotoSans-Regular.ttf");
    ui.fonts.insert_from_file(font_path).unwrap();

    // idを管理するための管理者の作成
    let ids = &mut Ids::new(ui.widget_id_generator());

    // gliumで描画するためのrendererの準備
    let mut renderer = conrod::backend::glium::Renderer::new(&display).unwrap();

    // The widgetとimageを結びつけて管理するmapping
    let image_map = conrod::image::Map::<glium::texture::Texture2d>::new();


    let mut num = "0".to_string();

    let mut event_loop = EventLoop::new();
    // windowのイベントループ
    'main: loop {
        for event in event_loop.next(&display) {
            // windowのイベントのハンドラーをuiにセット
            if let Some(event) = conrod::backend::winit::convert(event.clone(), &display) {
                ui.handle_event(event);
                event_loop.needs_update();
            }

            match event {
                // Escapeはwindowの削除用に
                glium::glutin::Event::KeyboardInput(
                    _,
                    _,
                    Some(glium::glutin::VirtualKeyCode::Escape),
                ) |
                glium::glutin::Event::Closed => break 'main,
                _ => {}
            }
        }


        set_widgets(ui.set_widgets(), ids, &mut num);

        // Uiの描画とその表示
        if let Some(primitives) = ui.draw_if_changed() {
            renderer.fill(&display, primitives, &image_map);
            let mut target = display.draw();
            target.clear_color(0.0, 0.0, 0.0, 1.0);
            renderer.draw(&display, &mut target, &image_map).unwrap();
            target.finish().unwrap();
        }
    }
}

// 配置するwidgetの配置方法の指定関数
fn set_widgets(ref mut ui: conrod::UiCell, ids: &mut Ids, num: &mut String) {
  // 背景(canvas)
    widget::Canvas::new()
        .pad(0.0)
        .color(conrod::color::rgb(0.2, 0.35, 0.45))
        .set(ids.canvas, ui);

    // canvasのidを使い指定することでuiからcanvasの横と縦の配列を取得
    let canvas_wh = ui.wh_of(ids.canvas).unwrap();


    // 数値の表示
    widget::Text::new(num)
        .middle_of(ids.canvas)
        .font_size(140)  // フォントのサイズを指定！！
        .color(color::WHITE)  // 色の指定も簡単にできる
        .set(ids.num_lbl, ui);

    // カウントボタン
    if widget::Button::new()
        .w_h(canvas_wh[0] - 10.0, 40.0)  // 幅
        .mid_bottom_with_margin_on(ids.canvas, 5.0)// 位置
        .rgb(0.4, 0.75, 0.6)  // 色
        .border(2.0)  // 境界
        .label("count +1")
        .set(ids.button, ui)
        .was_clicked()
    {  // if式の実行部分
        if let Ok(count) = num.parse::<u32>() {
            *num = (count+1).to_string();
        } else {
            println!("invalid number");
        }
    }

}

// イベントの管理用構造体
struct EventLoop {
    ui_needs_update: bool,
    last_update: std::time::Instant,
}

impl EventLoop {
    pub fn new() -> Self {
        EventLoop {
            last_update: std::time::Instant::now(),
            ui_needs_update: true,
        }
    }

    /// すべての更新対象となるイベントの為の順次取得用関数
    pub fn next(&mut self, display: &glium::Display) -> Vec<glium::glutin::Event> {
        // 60FPSより早くならないようにするために一つ前の更新対象から少なくても16ms待つことにしておく。
        let last_update = self.last_update;
        let sixteen_ms = std::time::Duration::from_millis(16);
        let duration_since_last_update = std::time::Instant::now().duration_since(last_update);  // 前回の更新時と今の時間差の取得
        if duration_since_last_update < sixteen_ms {
            std::thread::sleep(sixteen_ms - duration_since_last_update);
        }

        // イベント全体の取得
        let mut events = Vec::new();
        events.extend(display.poll_events());  // displayにおけるイベントの取得

        // displayで更新があればUiでのイベント更新は次に持ち越し
        if events.is_empty() && !self.ui_needs_update {
            events.extend(display.wait_events().next());
        }

        // イベントの更新の後処理
        self.ui_needs_update = false;
        self.last_update = std::time::Instant::now();

        events
    }

    // Uiで他のイベントの更新があるかないかを要求することをeventのループでは確認しておくこと

    // これはいくつかのUiを描画する最初のタイミングや更新を要求するタイミングで使われる。
    pub fn needs_update(&mut self) {
        self.ui_needs_update = true;
    }
}
```
![conrod.png](https://qiita-image-store.s3.amazonaws.com/0/167385/548c1c99-6940-b70b-e631-8a6fd36a7435.png)

## コードの説明
まず最初に今回のプログラムで使用する```Id```を管理するための構造体をマクロで生成します。

```rust:
widget_ids!(
    struct Ids {
        canvas,
        num_lbl,
        button,
    });
```
そしてよくある```window```の作成します。
次にレイアウトを管理する```ui```を作成して、必要な```assets```(今回はフォント)を```ui```に追加しています。そして```ui```用の```Id```を生成しています。
そして次にgliumで描画するためのrendererの準備をして、その時に使用する```widget```とその```widget```用の画像とを対応させる辞書を作成しています。
最後に```event```周り、データ変数と```ui```と```Id```を結び付ける、再描画関係を用意して```main```関数は終了です。
後は```widget```設置用の補助関数とよくある```event```ループの処理関数だと思うので説明は省略しても分かると思います。

## 所感
簡単に使ってみたところconrodはラベルのサイズや色の変更が簡単で細かく装飾したい人には良さそうなクレートだと思います。まだまだチュートリアルなどの情報が少なく調べにくいですが、比較的アセット、レイアウト、イベント、変更するデータの分離ができているクレートだと思います。
## まとめ
``Id``での管理の面倒臭さを除いても（慣れれば）比較的使いやすいライブラリだと思う。