🪟

UIレイアウトエンジンYogaをOpenSiv3Dへ導入してみた

2023/12/29に公開

Siv3D Advent Calendar 2023の23日目の記事です。

たまたまUnityのUI Toolkitを調べていたらそこにYogaが使われている[1]ことを知ったので、仕組みの勉強ついでにSiv3Dにも組み込んでみました。

Yogaとは

Facebookが作っている、オープンソースのレイアウトエンジンです。
Facebook系のフレームワークであるReact Native, ComponentKit, Lithoのレイアウト機能の実装部分を担っており、コア部分は C++ で実装されています。

https://yogalayout.dev/

ちなみに、ホームページにはPlaygroundがあるので実際に触って試してみることができます。

Playground

Siv3Dへ組み込み

ソースコードをGitHubに公開しました。

https://github.com/sthairno/Siv3DYogaTest

コンパイルにはvcpkgをインストールする必要があります。Yogaもvcpkgでパッケージ化されているのですが、そのYogaのバージョンが古すぎるうえに、READMEに

Yoga is additionally part of the vcpkg collection of ports maintained by Microsoft and community contributors. If the version is out of date, please create an issue or pull request on the vcpkg repository.

意訳:vcpkg上のバージョンが古かったらIssueかPull Requestを送ってね!

と書かれていていたので今回リポジトリにはsubmoduleで紐づけています。

さて、実装したのは主に4つです。

  • Widget: 灰色の四角が表示されるシンプルなウィジェット
  • Label: 色付きテキストが表示されるウィジェット
  • LayoutTree: レイアウトを計算,処理するツリー
  • WidgetTreeEditor: ウィジェットを編集するエディタ

使い方は次のとおりです。

  1. Widget,Labelを組み合わせて作りたいUIを作成
  2. 作成したUIから、LayoutTreeを作成する
  3. LayoutTree::calculateLayoutでレイアウトを計算
  4. Widget::draw()で描画

サンプル1:プログラムからUI生成

プログラム生成サンプル

Main.cpp
#include <Siv3D.hpp> // Siv3D v0.6.13

#include "Widget.hpp"
#include "Label.hpp"
#include "LayoutTree.hpp"

void Main()
{
    Scene::SetBackground(Palette::White);
    Window::SetStyle(WindowStyle::Sizable);

    // UIを作成
    auto rootWidget = std::make_shared<Widget>();

    // パディングを30pxに設定
    rootWidget->style().setPadding(facebook::yoga::Edge::All, facebook::yoga::StyleLength::points(30));

    // FlexDirectionをRow(水平方向)に設定
    rootWidget->style().setFlexDirection(facebook::yoga::FlexDirection::Row);

    // 折返しを有効にする
    rootWidget->style().setFlexWrap(facebook::yoga::Wrap::Wrap);

    // 0~9のサイズが変化する子要素を作成
    for (int i : Iota(10))
    {
        auto childWidget = std::make_shared<Widget>();
        auto label = std::make_shared<Label>(String{ U'0' + i }, Palette::Black);

        // labelを中央に配置
        childWidget->style().setJustifyContent(facebook::yoga::Justify::Center);
        childWidget->style().setAlignItems(facebook::yoga::Align::Center);

        // childWidgetのサイズを少しずつ大きくする
        auto size = facebook::yoga::StyleLength::points(50 + 10 * i);
        childWidget->style().setDimension(facebook::yoga::Dimension::Width, size);
        childWidget->style().setDimension(facebook::yoga::Dimension::Height, size);

        childWidget->children.emplace_back(std::move(label));
        rootWidget->children.emplace_back(std::move(childWidget));
    }

    // UIからLayoutTreeを構築
    LayoutTree tree{ rootWidget };

    while (System::Update())
    {
        // レイアウトを計算
        tree.calculateLayout(Scene::Size());

        // ウィジェットを描画
        rootWidget->draw();
    }
}

サンプル2:WidgetTreeEditorでUI編集

WidgetTreeEditorサンプル

Main.cpp
#include <Siv3D.hpp> // Siv3D v0.6.13

#include "imgui_impl_s3d/DearImGuiAddon.hpp"

#include "Widget.hpp"
#include "Label.hpp"
#include "LayoutTree.hpp"
#include "WidgetTreeEditor.hpp"

void Main()
{
    Addon::Register<DearImGuiAddon>(U"ImGui");
    Scene::SetBackground(Palette::White);
    Window::SetStyle(WindowStyle::Sizable);

    // 空のUIを作成
    auto rootWidget = std::make_shared<Widget>();
    
    // UIからLayoutTreeを構築
    LayoutTree tree{ rootWidget };

    // UIを編集するエディタ
    WidgetTreeEditor editor{ rootWidget };

    while (System::Update())
    {
        // レイアウトを計算
        tree.calculateLayout(Scene::Size());

        // ウィジェットを描画
        rootWidget->draw();

        // UIを編集
        if (editor.update())
        {
            // 変更があったらLayoutTreeを再構築
            tree.construct(rootWidget);
        }
    }
}

Yogaコード解読

https://github.com/facebook/yoga

公式ドキュメントにAPIの解説があると心強いですが、レイアウトの挙動に関する情報のみだったので諦めてソースコードを読んでいくことに。それでもコメントで丁寧なAPIに関する説明がなされていたので、想像よりは楽でした。

構成

Yogaのコア部分はC++ですが、フロントは様々な言語(Java,JSなど)で実装されています。
コア部分はC言語の関数群でラップされており、C言語の関数を呼び出せる言語であれば容易に実装ができるようになっているようです。

Javaの関数例

次に、Yogaを構成する主なコンポーネントについて紹介します。

YGNode

レイアウトの木構造を構成するのに必要なノードです。

  • ノード種別 (Default or Text)
  • 親子関係
  • スタイル (YGNodeStyle)
  • レイアウトの計算結果 (YGNodeLayout)
  • コンテキスト (自由に設定可能なvoidポインタ)
  • コールバック関数

...などが保存されています。

YGNode.h
YGNodeLayout.h

YGNodeStyle

ノードのスタイルに関する情報が入っており、この情報を元にレイアウトが決定されます。
ノードのスタイル

YGNodeStyle.h

YGNodeCalculateLayout

YGNodeStyleから与えられた情報をもとに、各ノードの位置とサイズを計算する関数です。
availableWidthavailableHeightに表示領域のサイズ(例:ウィンドウのサイズ)を与えると、例えば画面いっぱいに表示したいときにこのサイズが使われます。

YGNodeCalculateLayout (YGNode.h)

おわりに

やっぱり実際にいじってみるのは良いですね。おかげでUI Toolkitに対する深い理解が得られました。[2] 今後はUIのレイアウトをファイルに保存できるようにしたり、YogaのPlaygroundのようにWebでUIを編集できるようにしたいなと思っています。

また、コードリーディングにおいては先人の解説記事も参考にしました (感謝...!)
https://blog.engineer.adways.net/entry/2018/08/24/202254

脚注
  1. https://docs.unity3d.com/ja/2023.2/Manual/UIE-LayoutEngine.html ↩︎

  2. 代償として実装にかかる時間を浪費したのはさておき ↩︎

Discussion