RustでGUI 〜OrbTk編〜


コード全体はgithubのogata-k/GUI_cmp/example_orbtkを参考にしてください。

OrbTk

今回はOrbTkのReadMeRust のマルチプラットフォーム UI ライブラリ OrbTk の使い方のサイトを参考にしています。このサイトとは違い、再現性を高めるためにバージョン0.3.1-alpha1を指定して進めていきます。
また、話題のGUIツールキットOrbtkを読むも参考になるかもしれません。

どんなクレートか

OrbTkReadMeにあるようにECSパターンとリアクティブな関数型APIをベースとした、RedoxOSやLinux、Android、iOS、Web(cargo-nodeによるwasmアプリケーション化)、openBSDで機能するクロスプラットフォーム対応の高機能(G)UIツールキットです。さらに今回は扱いませんが、スタイルの指定にCSS風の指定が可能です。
FlutterやReact、Yewに影響をうけて開発されているようです。

Redoxプロジェクトから話が始まったため、以前はNightlyコンパイラでしか動かなかったのですが最近Stableコンパイラでも動くようになりました。そのためか最近SNSで話題に上がったりしていました。
さらに、描画をOS依存が少ないraqoteで対応しているのと、基本的なものをほぼスクラッチしているためCライブラリの依存がありません。そのため、クレートのインストールも簡単(のはず)です。(以前試行錯誤した影響があったかもしれない)

コード

先ほど紹介したRust のマルチプラットフォーム UI ライブラリ OrbTk の使い方にカウントアップの例があるので今回は違う例として、画面をクリックしたらその位置を表示するプログラムを書いてみました。(バージョン0.3.1-alpha1版のカウントアップの例はコード片としてmainからたどれるように残してあるので各自確認してください。)
ちなみに、カウントアップは次の画像のような見た目となります。

まずはmain関数です。

main.rs
use orbtk::api::*;
use orbtk::prelude::*;

use crate::click_point::ClickView;

mod click_point;

fn main() {
    Application::new()
        .window(|ctx| {
            Window::create()
                .title("OrbTk Example")
                // 端末画面の左上から(100, 100)の位置に配置
                .position((100.0, 100.0))
                // リサイズはさせない
                .resizeable(false)
                .size(300.0, 300.0)
                // buildでEntityを登録
                .child(ClickView::create().build(ctx))
                .build(ctx)
        })
        .run();
}

ここでやっていることは単純で、GUIアプリを表すApplicationに枠の設定と表示内容の中身(windowメソッド内のWindowClickViewのこと)を設定して起動しているだけです。

それではclick_pointモジュールに書かれたClickViewの実装を見てみます。説明に関してはコメントを読めば大体わかると思います。

src/click_point.rs
use std::cell::Cell;

use orbtk::prelude::*;

#[derive(Debug, Copy, Clone)]
enum ClickViewAction {
    // クリックされた箇所を通知するために包んでおく
    Click(Point),
}

#[derive(Default)]
pub struct ClickViewState {
    // 内部可変で扱うので各自状態で保持しておきたいものをCellで包む
    action: Cell<Option<ClickViewAction>>,
}

impl ClickViewState {
    // アクションの状態を更新するためのヘルパーメソッド
    fn set_action(&self, action: impl Into<Option<ClickViewAction>>) {
        self.action.set(action.into());
    }
}

impl State for ClickViewState {
    // 画面が更新されるたびに呼ばれるので適切に終了条件を指定してやる必要がある
    fn update(&self, context: &mut Context<'_>) {
        if let Some(action) = self.action.get() {
            match action {
                ClickViewAction::Click(p) => {
                    context
                        .child("click-text")
                        // 通常の文字列は対応していないので利用できる形に変換する
                        .set("text", String16::from(format!("({}, {})", p.x, p.y)));
                }
            }
        }
        // 通知イベントを削除
        self.action.set(None);
    }
}

// クリックイベントは以下のよう指定するだけでは作れず、
// buttonの実装の
// https://github.com/redox-os/orbtk/blob/develop/crates/widgets/src/button.rs
// を見る限り方法はあるはず
widget!(ClickView<ClickViewState>: MouseHandler{});

impl Template for ClickView {
    fn template(self, id: Entity, context: &mut BuildContext) -> Self {
        let state = self.clone_state();
        self.name("ClickView")
            .child(
                TextBlock::create()
                    .text("Click!")
                    .horizontal_alignment("center")
                    .vertical_alignment("center")
                    // selectorのうちidをセット
                    .selector(Selector::new().id("click-text"))
                    // buildでEntityを登録
                    .build(context),
            )
            .on_mouse_up(move |p| {
                println!("on mouse up");
                state.set_action(ClickViewAction::Click(p));
                false
            })
    }
}

この実装からわかるように表示用のアイテムはWidgetマクロによって作られたWidgetとそのbuildした結果によるEntityの組み合わせからなります。つまりすべてEntityとなります。(FlutterのWidgetから継承を除いたものみたい感じ)

実装は単純でReactのように、
1. イベント本体のAction
2. イベントを発行するTemplateトレイトの実装(コンポーネントの用意)
3. 現在発行されているイベントを元に状態を書き換えるStateトレイトの実装(コンポーネントの更新)
からなります。状態を更新する必要がないならそもそもイベントは必要ないので上記の2だけの実装になります。

これを実行すると次の画像のような結果を得ることができます。座標は左上から右下に増加していきます。

所感

マクロによってwidgetの実装がされているため補間が効かず、IDEとの相性は最悪でした。
そのうえ、プロパティやIdの指定が文字列指定なのでこちらも補間が効きません。
感覚としては補間が効かないHTMLのような感じです。
まだまだ日本語情報が少ないのとAPIが頻繁に変わる点も学習にはネックになってきます。
しかし、2020/02/27現在examplesが問題なく動くことは学習にはとてもありがたいです。
また、表示と状態と処理の分離を強制させるようになっているので、興味の分離ができるという点もGUIの複雑な画面を作るには向いているように思えます。

まとめ

RedoxOSやAndroid、iOS、Webでも動くというUIKitとしての強みがあるのとイベントが扱いやすいことから、今後主流になってもおかしくないクレートです。