はじめに
Slintアドベントカレンダーも6日目に突入しました。昨日は、@task_jpさんによる「Toradex Verdin AM62 Dual で meta-slint を動かしてみた」でした。実はmeta-slint周り書こうと思って準備してたのですけど先を越されました。
Slintは組込み利用だと商用ライセンス一択(まぁ、GPLv3で良いなら別ですが)なので、いきなり組込みで大人気になるところまでは行かない気もするのですが、軽量・省メモリと組込みで一番気を使うところに注力しており、Rustで作られていることから、組込みでも徐々に注目度が上がっていくと良いなぁと思っています。
SlintはRustの他C++やJavaScript,Pythonから利用できるのですが、meta-slintは、YoctoでSlintのC++向けのランタイムライブラリ周りを構築するためのレイヤです。Yoctoは組込み向けのLinuxディストリビューションを構築するための"メタディストリビューション"にあたり、クロスビルド用のツールチェーンの構成、ホストツールをビルドし、各種サービス、ランタイム、ソフトウェアをクロスビルドをしながら組込みLinuxディストリビューションのルートイメージを構築して行きます。
Slintのランタイム・コンパイラ類はRust言語により構築されているのですが、ちょっと面倒なのはRust自体もクロスビルドのための仕組みやパッケージ構築システムを独自に持っていて、うまく行かないと自分が何をやっているのか、どこでつまずいているのかわからなくなるのですよね。
割と新しい言語であるRustは、昨今のセキュリティに関する要請、Linuxドライバ向けの機能提供もあってか、Yoctoへ組み込まれつつあり、現在はちょうど変遷期で新旧の情報が入り乱れているので、軽い気持ちで始めるとドハマりします(なにせビルド時間が長いですからねぇ)。こうやって成功例が提示されているのはありがたいことです。
ちなみに、最新のYocto LTSだとRustはopenembeded-core側に組み込まれています。このためmeta-rustは使いません。RUSTを使うことを明示すれば、そのままSlintが動かせるはずです(というかmeta-rustがあると失敗する)。うまく行ったらアドベントカレンダーに手順を書こうかなと思いますので、期待せずお待ちください。
本日のお題
さて、今年からアドベントカレンダーが作成されていくつか記事を書いて来ましたが、そもそもSlint自体皆さんよく知らないかと思います。そこでSlintに関して主にGUIを構築するSlint言語を中心に入門を書いてみようかなと思います。基本的にSlintのREADME.mdやドキュメントの焼き直しですけど、もう少し読みやすい並びにできないかなということで整理してみたいと思います。
いや、君、いつも(1)とかつけると続きかかないじゃんって怨嗟の声が聞こえた気もしますが、聞かなかったことにしてとりあえず書いていきましょう。
まずは事前準備でSlintPadを使って見よう
Slintにはいくつか便利なツールがありますが、SlintPadはブラウザさえあればSlint言語のUIイメージをエディット・プレビューできるWebアプリケーションです。
ソースコードも開示されているので、気になるかたはそちらも見てみると良いでしょう。Slintは気になったけど、いきなり色々インストールはめんどくさいという人はまずSlint言語がどんな感じなのか軽くUIを作ってみるのにこちらを使って見ることをおすすめします。サイトにアクセスすると、以下のような画面が開きます。
左側がエディタ、右側がプレビューです。プレビュー側はエディットスイットを操作するとデザイナー的に利用することも可能です。
最初はHello WorldテキストとAbout Slintが表示されているかと思います。エディタ側ですべてを一度削除し、以下のコードを入れてみましょう。
component MyText inherits Text {
font-size: 20pt;
color: black;
}
export component MyApp inherits Window {
preferred-width: 200px;
preferred-height: 80px;
Rectangle {
width: 200px;
height: 80px;
background: green;
MyText {
x:0;y:0;
text: "Hello";
}
MyText {
y:0;
x: 75px;
text: "World";
}
}
}
右側のプレビューが更新されたはずです。続いてProjectという場所をクリックし、add fileでmytext.slintというファイルを追加し、main.slintの先頭のMyTextコンポーネントをカット&ペーストします。
component MyText inherits Text {
font-size: 20pt;
color: black;
}
続いてmain.slintの先頭に以下の行を追加します。
import { MyText } from "mytext.slint";
このように.slintファイルの分割にも対応しています。Slint言語入門をやっていきますが、基本的にはこのSlintPadで表現できる範囲にする予定です。試してみたい場合はこちらをご利用いただければと思います。
Slint言語入門
SlintはQMLにインスパイアされた軽量なGUIフレームワークです。宣言型UI言語を使いGUI画面をデザインし、Rust/C++/JavaScript/Pythonなどでビジネスロジックを構築することでGUIアプリケーションを構築できます。
本書では、Slintのデザインコードを作成するためのSlint言語に焦点をあてて解説します。
アーキテクチャ
Slintでは、Slint言語(または.slint言語と表記されている箇所もある)と呼ぶ独自の宣言型UI言語で画面をデザインし、コンパイラ(または実行時に動的解釈するインタプリタ)でネイティブ言語やオブジェクトへコンパイルし、対象言語で実装されたビジネスロジックと組み合わせでGUIアプリケーションを構築できます。
Slintコンパイラ
Slintコンパイラは、典型的なコンパイラと同じく、字句解析、構文解析、最適化、そしてコード生成というフェーズを実行します。ターゲット言語でのコード生成用にさまざまなバックエンドを提供しており、C++コードジェネレーターは C++ ヘッダーファイルを生成、RustジェネレーターはRustコードを生成します。動的言語用インタープリターも用意されています。
Slintではローエンドの機器でも動作するよう、事前コンパイルでネイティブ変換することを基本としています。これにより最適化を行い、レンダリング時間の短縮をはかっています。また、将来に向けては画像やテキストの事前処理を行い、よりパフォーマンスを向上していけないか検討が進められています。
レンダリングバックエンドとウィジェットスタイルなどはコンパイルの段階で決定されます。
Slintランタイム
Slintランタイムは、Slint言語で宣言されたプロパティをサポートするエンジンで構成されています。エレメントやアイテム、プロパティ等を含むコンポーネントは、メモリ割り当てを削減するため単一のメモリ領域に配置されます。
モジュールとコンポーネント
Slintでは、ユーザーインターフェースをSlint言語で記述し、.slintの拡張子を持つファイルに保存します。この.slintファイルには1つまたは複数のコンポーネント(部品)を宣言できます。コンポーネントはエレメント(要素)のツリーを持つことができ、このエレメントでGUIを定義していくことになります。
Slintではコンポーネントが基本となりGUIを構成します。コンポーネントは独自の再利用可能なUIコントロールセットを構成します。宣言された各コンポーネントは、別コンポーネントのエレメントとして使用できます。
以下は、コンポーネントの利用例です。
component MyText inherits Text {
font-size: 20pt;
color: black;
}
export component MyApp inherits Window {
preferred-width: 200px;
preferred-height: 80px;
Rectangle {
width: 200px;
height: 80px;
background: green;
MyText {
x:0;y:0;
text: "Hello";
}
MyText {
y:0;
x: 75px;
text: "World";
}
}
}
組込みエレメントであるTextを継承したMyTextと、Windowを継承したMyAppがコンポーネントとして宣言されています。MyAppはMyTextをエレメントツリーで利用されています。
.slintの拡張子を持つファイルはモジュールという扱いとなり、内包するコンポーネントをexportすることで、別モジュールからimportして利用することができます。 SlintPadの項目で説明したように以下のようにモジュールを分割できます。
export component MyText inherits Text {
font-size: 20pt;
color: black;
}
import { MyText } from "mytext.slint";
export component MyApp inherits Window {
preferred-width: 200px;
preferred-height: 80px;
Rectangle {
width: 200px;
height: 80px;
background: green;
MyText {
x:0;y:0;
text: "Hello";
}
MyText {
y:0;
x: 75px;
text: "World";
}
}
}
なお、Slint 1.8.0現在、最上位にあたる視覚エレメントはWindowまたはDialogを継承したものであることが期待されています。
エレメントとプロパティ
SlintのエレメントはUIを表現する基本的な構成要素です。Slintには標準的な組込みエレメントが用意されており、例示で利用しているようなRectangleやTextなど視覚的なエレメント以外にも状態やタイマーといった視覚的要素を持たないエレメントも存在します。
エレメントには、それぞれ固有のプロパティがあり、エレメントの値や状態を保持する変数として機能します。プロパティには値や式を割り当てることができます。これらのプロパティにはリアクティブな特性があり、依存するプロパティが変更されるとその影響を受けるプロパティやUIが自動的に再評価・更新されます。
また、プロパティの評価は実際に使用される段階まで評価されないという遅延評価の特性もあり、無駄な計算を排除しパフォーマンスの低下を防ぎます。
プロパティ間の依存関係は評価中に自動的に検出されます。プロパティは評価の結果を保存します。プロパティが変更されると、すべての依存プロパティに通知され、次に値が読み取られるときにバインディングが再評価されます。
このようにプロパティ評価は遅延型でリアクティブであるため、適切に動作するにはプロパティを評価してもプロパティ自体以外の観測可能な状態は変更されない必要があります。この場合の式は「pure」であり、そうでない場合には「副作用がある」と言われます。
副作用はいつ発生するかが必ずしも明確ではありません。遅延評価によって副作用の順序が変わったり、副作用が発生するかどうかに影響したりすることがあります。さらに副作用によるバインディング評価中のプロパティの変更は、予期しない動作を引き起こす可能性があります。このため、Slint のバインディングは「pure」でなくてはなりません。
Slintコンパイラは、pureなコンテキストが副作用を持たないように強制します。pureなコンテキストには、バインディング式、pureな関数の本体、pureなコールバックハンドラーが含まれます。このようなコンテキストでは、プロパティを変更したり、pureでないコールバックや関数を呼び出したりすることはできません。
コールバックとパブリック関数にpureキーワードを注釈付けして、バインディングやその他のpureなコールバックや関数からアクセスできるようにできます。プライベート関数のpureについては自動的に推論されますが、プライベート関数を明示的にpureと宣言して、コンパイラにその純粋性を強制させることもできます。
export component Example {
pure callback foo() -> int;
public pure function bar(x: int) -> int
{ return x + foo(); }
}
エレメントと関数
プロパティには式を入れることができることを説明しました。この式に名前をつけてcomponentまたはエレメンツツリー内部に関数として定義することができます。
関数宣言
Slint言語の関数はfunctionキーワードを使って宣言します。
export component Example {
// ...
function my-function(parameter: int) -> string {
// Function code goes here
return "result";
}
}
export component TextExample {
Text {
function double(x: int) -> int {
return x * 2;
}
}
}
関数は、"name: type"の形式で引数を設定することができます。また" -> type"の指定で戻り値型を定義し、return文を使うことで値を返すことが可能になります。
関数呼び出し
関数はエレメント名付きでも、エレメント名なしでも呼び出し可能です。
import { Button } from "std-widgets.slint";
export component Example {
// Call without an element name:
property <string> my-property: my-function();
// Call with an element name:
property <int> my-other-property: my_button.my-other-function();
pure function my-function() -> string {
return "result";
}
Text {
// Called with a pre-defined element:
text: root.my-function();
}
my_button := Button {
pure function my-other-function() -> int {
return 42;
}
}
}
アクセス制限
Slint言語では、デフォルトではプライベート関数としてそのコンポーネントでのみ利用が許可されます。
関数の定義にprotectedをつけることでその関数を有するコンポーネントを継承したコンポーネントに公開することが可能です。
component PComponent
{
protected function my-function(parameter: int) -> string {
return "result";
}
}
component CComponent inherits PComponent
{
property<string> my-prop : my-function();
}
また関数の定義にpublicをつけると、他のコンポーネントに公開することが可能です。
export component HasFunction {
public pure function double(x: int) -> int {
return x * 2;
}
}
export component CallsFunction {
property <int> test: my-friend.double(1);
my-friend := HasFunction {
}
}
一点注意が必要なのは、エレメントツリーに定義した関数です。
export component HasFunction {
t := Text {
public pure function double(x: int) -> int {
return x * 2;
}
}
}
export component CallsFunction {
property <int> test: my-friend.t.double(1);
my-friend := HasFunction {
}
}
上記はmy-friend.t.double(1)呼び出しでコンパイルエラーが発生します。double関数はpublicですが、t:= Textはprivateなため別コンポーネントにアクセス許可が無いためです。
エレメントの配置とレイアウト
Slintでは、視覚エレメントは左上を(0,0)とし、右下を(width,height)とする座標系で管理されます。xおよびyプロパティは、親要素を基準とした要素の座標を格納します。Slintは、要素の位置に親の位置を追加することで、要素の絶対位置を決定します。親に親要素がある場合は、その要素も追加されます。この計算は、最上位の視覚要素に到達するまで続けられます。
またwidthおよびheightプロパティは視覚要素のサイズを格納します。
視覚的エレメントのサイズ・配置は、以下の2種類の方法があります。
- プロパティで明示的に指定する
- Layoutエレメントで自動的に配置する
プロパティによる明示的な配置
以下は、x,y,width,heightを使った明示的な配置例になります。
export component Example inherits Window {
width: 200px;
height: 200px;
Rectangle {
x: 100px;
y: 70px;
width: parent.width - self.x;
height: parent.height - self.y;
background: blue;
Rectangle {
x: 10px;
y: 5px;
width: 50px;
height: 30px;
background: green;
}
}
}
- ウィンドウのサイズは(200x200)となっています
- 内側に青色の矩形を配置しています。座標とサイズを計算すると (100,70), (100x130)となります
- 青の内側に緑の矩形を配置しています。座標とサイズは(10,5),(50x30)となります
座標は親の内側で親の左上が(0,0)となります。ですので、緑矩形の位置における青矩形内の(10,5)座標はWindow座標においては(110,75)となります。
サンプルではwidth/heighとも固定値にしていますが、サイズ指定を推奨値にして可変にできます。
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
:
こうするとPreviewの部分でウィンドウの枠を掴み、拡大・縮小できるようになります。内側の青矩形サイズは式を指定して親のサイズプロパティから計算しています。そのため、ウィンドウの拡大縮小でサイズが変動します。緑矩形は固定サイズのため青矩形の左上座標から(10,5)の位置でサイズはそのままに位置します。
ちなみに、サイズは親のサイズとの相対値を%指定で設定することもできます。緑矩形のサイズを変更してみます。
Rectangle {
x: 10px;
y: 5px;
width: 25%;
height: 25%;
background: green;
}
こうすると、親のサイズの25%になるように自動調整されます。
なお、X,yプロパティを削除すると、自動的に親の中央に位置するように配置されるようになります。
Rectangle {
width: 25%;
height: 25%;
background: green;
}
同様に、width/heightプロパティを削除すると、自動的に親サイズに対し100%として動作します。
レイアウトによる自動的な配置
Slintではエレメントの配置を自動的に行うレイアウトというエレメントが用意されています。
- VerticalLayout (垂直配置)
- HorizontalLayout (水平配置)
- GridLayout (格子配置)
まずは、単純に4つの矩形がある状態を作ります。
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
Rectangle { background: green;}
Rectangle { background: black;}
Rectangle { background: yellow;}
Rectangle { background: red;}
}
座標、サイズとも指定がないので、すべての矩形が親と同じサイズで中央に配置されます。重ね合わせが発生するので、一番最後の赤色矩形が表示されることになります。そこで、RectangleをVerticalLayoutで括ってみます。
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
VerticalLayout {
Rectangle { background: green;}
Rectangle { background: black;}
Rectangle { background: yellow;}
Rectangle { background: red;}
}
}
VerticalLayoutの子が垂直に配置されました。HorizontalLayoutに変更すると
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
HorizontalLayout {
Rectangle { background: green;}
Rectangle { background: black;}
Rectangle { background: yellow;}
Rectangle { background: red;}
}
}
垂直に並びます。Gridを使う場合は更にプロパティ指定かRowエレメントを使う必要がありますが
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
GridLayout {
Row {
Rectangle { background: green;}
Rectangle { background: black;}
}
Row {
Rectangle { background: yellow;}
Rectangle { background: red;}
}
}
}
格子状に配置することが可能です。細かな利用方法やパラメータについては、後ほど解説します。今は配置用のレイアウトというシステムがあるとだけ思っておいてください。
エレメントツリーと識別子
これまで見たとおり、エレメントは入れ子にすることができます。
component MyText inherits Text {
font-size: 20pt;
color: black;
}
export component MyApp inherits Window {
preferred-width: 400px;
preferred-height: 300px;
Rectangle {
width: 90%;
height: 90%;
background: green;
VerticalLayout {
width: 90%;
height: 90%;
MyText { text: root.width/1px; }
MyText { text: parent.width/1px; }
MyText { text: self.width/1px; }
}
}
}
この例ではMyAppコンポーネントに以下のようなエレメントツリーが構築されます。
- MyApp
- Rectangle
- VerticalLayout
- MyText
- MyText
- VerticalLayout
- Rectangle
MyTextはVerticalLayoutの子、VerticalLayoutはMyTextの親という関係性です。
この時、自分自身はself、 親はparent、最上位はrootという識別子で参照できます。
ところで、rootでも、parentでもないRectangleにアクセスしたい場合があります。この場合Rectangleに識別子を設定できます。
export component MyApp inherits Window {
preferred-width: 400px;
preferred-height: 300px;
box := Rectangle {
width: 90%;
height: 90%;
background: green;
VerticalLayout {
width: 90%;
height: 90%;
MyText { text: root.width/1px; }
MyText { text: box.width/1px; }
MyText { text: parent.width/1px; }
MyText { text: self.width/1px; }
}
}
}
boxという名称をRectangleに設定してMyTextではそのwidthを取得しています。
まとめ
今回は、Slint言語を学ぶ上で簡単にお試しできるSlintPadを簡単にご紹介したうえで、Slint言語の基礎となる概念を簡単に説明しました。公式ドキュメントでは主にREADME.mdとコンセプトの一部ですかね。
- モジュール
- コンポーネント
- エレメント
- レイアウト
- エレメントツリーと識別子
次回は、Slint言語のプリミティブ型と構造体、列挙体、配列周りについて、簡単に解説できればと思います。
なお、機会があれば技術同人誌の原稿としても使いたいなと思っているので、説明の順番が悪いとか、説明が悪いとか、ご意見があればどしどしコメントしてください。
明日は @task_jp さんによる「Raspberry 5 で Slint のアプリを動かす」です。楽しみにお待ちしましょう。