なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

Unity の UXML + USS で Media Query を実現する

みなさん Unity 2019 辺りから搭載されてる UXML と USS 使っていますか?
Web で調べてもほぼほぼ情報が出てこないこいつ、 Web 界隈であり WPF 界隈でもあったわたしからすれば天国のような開発環境なんですが、
CSS の弱めのサブセットということでしかなく、 CSS ではできるあれやこれやが出来なくてもやもや~とすることがあります。

今回はそのうちの 1 つである Media Query を Unity で実装しよう!というお話です。
例えば、画面幅が 1280px 以下である場合とそれ以上である場合で UI レイアウトを変えたい場合、 CSS では以下のようにします。

.foo {
  width: 100%;
}

@media (min-width: 1280px) {
  .foo {
    width: 920px;
  }
}

これで 1280px より小さい場合は .foo を当てたスタイルは幅が 100%に、それ以上の場合は 920px になります。
USS は CSS のサブセットなのであるかも?とは思いますが、少なくとも軽く調べた限りでは対応していないようでした。
ということで、 C# スクリプトの力を使って、無理矢理解決してみます。

まずは、すでに適用されている USS を取り込みます。これは、以下のようなコードで可能です。
このとき、 UXML 側に差し込む USS には @media なメディアクエリを書いてある状態のものを渡します。

private void LoadStyleSheets(EditorWindow window)
{
    if (window.rootVisualElement.styleSheets.count <= 1)
        return;

    for (var i = 0; i < window.rootVisualElement.styleSheets.count; i++)
    {
        var styleSheet = window.rootVisualElement.styleSheets[i];
        //
    }
}

次は、取り出した USS をパースします。
これ自体は正規表現をこねくり回してやってあげます。

private static readonly Regex Comment = new Regex("\\/\\*[^*]*\\*+([^/][^*]*\\*+)*\\/", RegexOptions.Compiled);
private static readonly Regex Media = new Regex("@media[^\\{]+\\{([^\\{\\}]*\\{[^\\}\\{]*\\})+", RegexOptions.Compiled);
private static readonly Regex MaxWidth = new Regex("\\(\\s*max\\-width\\s*:\\s*(\\s*[0-9\\.]+)(px|rem)\\s*\\)", RegexOptions.Compiled);
private static readonly Regex MinWidth = new Regex("\\(\\s*min\\-width\\s*:\\s*(\\s*[0-9\\.]+)(px|rem)\\s*\\)", RegexOptions.Compiled);
private static readonly Regex MinMaxHxW = new Regex("\\(\\s*m(in|ax)\\-(height|width)\\s*:\\s*(\\s*[0-9\\.]+)(px|rem)\\s*\\)", RegexOptions.Compiled);
private static readonly Regex Other = new Regex("\\([^\\)]*\\)", RegexOptions.Compiled);
private static readonly Regex Styles = new Regex("@media *([^\\{]+)\\{([\\S\\s]+?)$", RegexOptions.Compiled);

private void TranslateStyleSheet(StyleSheet stylesheet)
{
    var raw = ...; // read stylesheet as string
    var css = Comment.Replace(raw, "");

    foreach (Match queryString in Media.Matches(css))
    {
        if (Styles.IsMatch(queryString.ToString()))
        {
            var match = Styles.Match(queryString.ToString());

            foreach (var query in match.Groups[1].Value.Split(','))
            {
                if (Other.IsMatch(MinMaxHxW.Replace(query, "")))
                    continue;

                _styles.Add(new {
                    MinWidth = ..., // 正規表現にマッチした部分から min-width を取り出す
                    MaxWidth = ..., // 正規表現にマッチした部分から max-width を取り出す
                    Rule = match.Groups[2].Value,
                });
            }
        }
    }
}

あとは、取りだしたものを動的に割り当ててあげれば終わりです。

private void ApplyMediaQueries(EditorWindow window)
{
    var rules = ...; // 今のウィンドウサイズに該当するルールだけ適当に取り出す
    var root = window.rootVisualElement;

    // この辺で動的に当てたものをクリアする

    if (rules.Count == 0)
        return;

    var asset = ScriptableObject.CreateInstance<StyleSheet>();
    asset.hideFlags = HideFlags.NotEditable;
    asset.name = DynamicGeneratedStyleSheetName;

    var sb = new StringBuilder();
    foreach (var rule in rules)
        sb.AppendLine(rule.Rule);

    // Importer 経由で文字列から動的にメモリ上にアセットを作成する
    var t = typeof(AssetDatabase).Assembly.GetType("UnityEditor.StyleSheets.StyleSheetImporterImpl");
    var i = Activator.CreateInstance(t);
    var m = t.GetMethod("Import", BindingFlags.Public | BindingFlags.Instance);
    if (m == null)
        return;

    m.Invoke(i, new object[] { asset, sb.ToString() });

    root.styleSheets.Add(asset);
}

簡単ですね。これだけで、良い感じに Media Query が実装できます。
ということで、これを実装したものを最後において、今日のメモでした!

https://github.com/natsuneko-laboratory/web-polyfill