Rust のマルチプラットフォーム UI ライブラリ OrbTk の使い方

f:id:Naotsugu:20191214174841p:plain


OrbTk とは

Redox という Rust で書かれた UNIX ライク OS のサブプロジェクトとして開発されている Widget Toolkit です。

OrbTk の機能としては以下が謳われています。

  • モダンな軽量API
  • クロスプラットフォーム
  • モジュール化されたクレート
  • Entity-Component-System ライブラリ DCES に基づく
  • 柔軟なイベントシステム
  • 包括的なウィジェットライブラリ
  • カスタムウィジェットのサポート
  • テーマのサポート
  • デバッグツールの統合


2019年12月時点でのバージョンは 0.3.1-alpha1 となっており、まだまだ開発途上ではありますが、マルチプラットフォームな GUI アプリケーションを簡単に作ることができます。

本記事では OrbTk の使い方を簡単に見ていきます。


プロジェクトの作成

Cargo でプロジェクトを作成します。

$ cargo new --bin orbtk-example
     Created binary (application) `orbtk-example` package
$ cd orbtk-example


Cargo.toml には、github のリポジトリ(開発ブランチ)から依存を追加します。

[dependencies]
# orbtk = "0.3.1-alpha1"
orbtk = { git = "https://github.com/redox-os/orbtk.git", branch = "develop" }

現時点のリリースバージョンは 0.3.1-alpha1 ですが、開発途上のため、頻繁に API が変更されています。

ここでは最新の開発ブランチを使うことにしました。


Hello OrbTk

最初にウインドウを表示するだけのアプリケーションを作りましょう。

main.rs を以下のように変更します。

use orbtk::prelude::*;

fn main() {
  Application::new()
    .window(|ctx| {
      Window::create()
        .title("OrbTk Example")
        .position((100.0, 100.0))
        .size(300.0, 300.0)
        .child(TextBlock::create().text("Hello OrbTk").build(ctx))
        .build(ctx)
    })
    .run();
}

ウインドウを作成し、TextBlock を追加しただけの簡単なものです。


実行します。

$ cargo run

以下のようなウインドウが表示されます。

f:id:Naotsugu:20191214172749p:plain


Template によるレイアウト定義

もう少し複雑な例として、Template によるレイアウトを定義してみます。

use orbtk::prelude::*;

impl Template for MainView {
  fn template(self, _: Entity, ctx: &mut BuildContext) -> Self {
    self.name("MainView").child(
      Stack::create()
        .child(
          TextBlock::create()
            .margin((0.0, 0.0, 0.0, 8.0))
            .text("Stack vertical")
            .selector("h1")
            .build(ctx),
        )
        .child(
          Button::create()
            .text("center")
            .horizontal_alignment("center")
            .build(ctx),
        )
        .build(ctx),
    )
  }
}

widget!(MainView);

fn main() {
  Application::new()
    .window(|ctx| {
      Window::create()
        .title("OrbTk Example")
        .position((100.0, 100.0))
        .size(300.0, 300.0)
        .child(MainView::create().build(ctx))
        .build(ctx)
    })
    .run();
}

Template にてコンポーネントを宣言的に定義し、widget! マクロに渡します。

ウインドウの子要素として MainView::create() として渡すことで、定義した UI を追加します。


先ほどと同様に実行すると以下のような画面が表示されます。

f:id:Naotsugu:20191214173651p:plain


イベント処理

先ほどの例にボタンのクリック時のイベントを追加してみましょう。

main.rs を以下のように変更します。

use orbtk::prelude::*;
use std::cell::Cell;

#[derive(Debug, Copy, Clone)]
enum Action {
  Increment,
}

#[derive(Default, AsAny)]
pub struct MainViewState {
  count: Cell<usize>,
  action: Cell<Option<Action>>,
}

impl MainViewState {
  fn action(&self, action: impl Into<Option<Action>>) {
    self.action.set(action.into());
  }
}

impl State for MainViewState {
  fn update(&mut self, _: &mut Registry, ctx: &mut Context<'_>) {
    if let Some(action) = self.action.get() {
      match action {
        Action::Increment => {
          let result = self.count.get() + 1;
          self.count.set(result);
          ctx.child("text-block").set("text", String16::from(result.to_string()));
        }
      }
      self.action.set(None);
    }
  }
}


impl Template for MainView {
  fn template(self, id: Entity, ctx: &mut BuildContext) -> Self {
    self.name("MainView").child(
      Stack::create()
        .child(
          TextBlock::create()
            .margin((0.0, 0.0, 0.0, 8.0))
            .text("Stack vertical")
            .selector(
              Selector::from("text-block").id("text-block"),
            )
            .build(ctx),
        )
        .child(
          Button::create()
            .text("center")
            .horizontal_alignment("center")
            .on_click(move |states, _| -> bool {
                state(id, states).action(Action::Increment);
                true
            })
            .build(ctx),
        )
        .build(ctx),
    )
  }
}

widget!(MainView<MainViewState> {
    counter: usize
});


fn main() {
  Application::new()
    .window(|ctx| {
      Window::create()
        .title("OrbTk Example")
        .position((100.0, 100.0))
        .size(300.0, 300.0)
        .child(MainView::create().build(ctx))
        .build(ctx)
    })
    .run();
}


fn state<'a>(id: Entity, states: &'a mut StatesContext) -> &'a mut MainViewState {
    states.get_mut(id)
}


実行すると以下のようにボタンクリックをカウントします。

f:id:Naotsugu:20191214174613g:plain


先の例を簡単に説明していきます。


ボタンクリック時のアクションを以下の enum にて定義しています。

#[derive(Debug, Copy, Clone)]
enum Action {
    Increment,
}


このアクションは、View の状態変更時に以下のパターンマッチにて処理しています。

impl State for MainViewState {
  fn update(&mut self, _: &mut Registry, ctx: &mut Context<'_>) {
    if let Some(action) = self.action.get() {
      match action {
        Action::Increment => {
          let result = self.count.get() + 1;
          self.count.set(result);
          ctx.child("text-block").set("text", String16::from(result.to_string()));
        }
      }
      self.action.set(None);
    }
  }
}


MainView には以下のようなクリックカウントとボタンクリック時のアクションを状態として定義しています。

#[derive(Default, AsAny)]
pub struct MainViewState {
  count: Cell<usize>,
  action: Cell<Option<Action>>,
}

impl MainViewState {
  fn action(&self, action: impl Into<Option<Action>>) {
    self.action.set(action.into());
  }
}


ボタンには on_click でクリック時に Increment アクションを設定しています。

  Button::create()
    .text("center")
    .horizontal_alignment("center")
    .on_click(move |states, _| -> bool {
        state(id, states).action(Action::Increment);
        true
    })
    .build(ctx),

ボタンクリックにより MainViewState にアクション内容を反映し、MainViewStateupdate() にてアクションに応じた処理を行う流れになります。


まとめ

OrbTk を使った簡単なアプリケーション作成の流れを見ました。

現時点ではドキュメントなどはほとんど整備されておらず、API の変更も頻繁です。

なので、おもちゃとして触る程度になりますが、将来的には Linux で言う GTK のような扱いになるはずなので、少しずつでもウォッチしていきたいですね。



プログラミングRust

プログラミングRust

  • 作者:Jim Blandy,Jason Orendorff
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/08/10
  • メディア: 単行本(ソフトカバー)

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Rust in Action

Rust in Action

  • 作者:Tim McNamara
  • 出版社/メーカー: Manning Publications
  • 発売日: 2020/05/12
  • メディア: ペーパーバック