コンテンツにスキップ

見た目とウィンドウ

要素そのものの話のあとに残るのは二つです。 画面をどう並べて何色にするか、そして、要素ではなくウィンドウが用意しているものです。

並べる

columnrow は、子を上下と左右に並べます。 並べかたの違うコンテナがもう四つあり、どれも column と同じように、子を引数としてもブロックとしても取ります。

  grid(
    text("one"), text("two"),
    grid_cell(text("across both", align: "center"), col_span: 2),
    text("three"), text("four"),
    columns: 2, spacing: 6.0
  )

  stack(
    image("assets/postcard.png", width: 180.0, height: 90.0),
    text("over the picture", background: "#f9e2af", padding: 4.0)
  )

  scroll_view(column(*(1..12).map { |n| text("line #{n}") }), height: 90.0)
  h_scroll_view(row(*(1..10).map { |n| text("col #{n}", width: 70.0) }))

grid は子を columns: 本の列に並べます。 一つの子が複数の列にまたがるときは、その子に col_span: を書くか、grid_cell で包みます。 どちらの書き方でも同じ木になります。 stack は子を書いた順に重ねます。 スクロールする二つの面は、子が収まらないときにスクロールします。

open: が真のあいだ、modal はウィンドウの手前に重なって出ます。 ダイアログはこれで書きます。 出すかどうかを決めるのはアプリです。

  modal(open: @asking) {
    column(spacing: 8.0, padding: 14.0) {
      text "delete the file?"
      row(spacing: 8.0) {
        button("cancel") { @asking = false }
        button("delete") { remove }
      }
    }
  }

demo/panels.rb が四つのコンテナを一画面に、demo/dialog.rb が modal を見せます。

小さな要素

  spacer(grow: 1.0)                     # 親の余りを取る
  divider(thickness: 2.0, color: "accent")
  spinner(size: 18.0)                   # 終わりの見えない処理に
  progress(0.4, label: "copying…")      # 0 から 1 まで満ちるバー
  progress(0.0, indeterminate: true)    # 同じ処理を、往復で見せる
  link("Website", "https://example.org")
  image("assets/postcard.png", width: 180.0)
  svg("assets/search.svg", width: 20.0, height: 20.0)

row の中の spacer は、続くものを端まで押しやります。 column の中では同じことを下に向かって行います。 link にハンドラがないのは、ページを開くことがアプリの状態ではないからです。

すべての要素が取るキーワード

どの要素も、次の共通のプロパティを同じ名前と同じ意味で取ります。

キーワード はたらき
width height 要素が何であれ、外側の箱が取る大きさ
min_width max_width その幅の下限と上限
disabled 真偽 触れない見た目になり、入力を受け取らない
theme "dark""light" この要素から下で使う配色
animate 変化にかける時間、ミリ秒
easing "linear" "in" "out" "inOut" その変化の速さの付け方
enter exit 真偽 現れるとき、消えるときにも動かす
col_span row_span 整数 グリッドで何列ぶん、何行ぶんを占めるか
role 文字列 画面読み上げがこれを何と呼ぶか
a11y_label 文字列 画面読み上げが表示文字の代わりに読む名前
tooltip 文字列 ポインタを重ねたときに出る説明
  button("save", disabled: @busy, tooltip: "write the file", width: 120.0)
  text "total", role: "heading", a11y_label: "the running total"

これらは三十個の要素にフィールドを繰り返したものではなく、要素を包む形で作ってあります。 だから elements.toml に一度だけ書けて、どこでも同じ意味になります。

同じ名前をもともと自分の意味で持っている要素では、要素側の意味がそのまま通ります。 textwidth は文字の幅を指し、imagewidthheight は画像そのものの大きさなので、共通のプロパティは手を出しません。 どの要素がそうなのかは要素に印をつけてあります。

demo/shared.rb は、この共通のプロパティを一つずつ、違う種類の要素に付けたものです。 spacer にテーマ、選択にアニメーション、区切り線に説明、フィールドに二列ぶんの幅、そして欄とボタンを操作できなくする disabled です。

色は 16 進の文字列("#f38ba8")か、配色の中の名前です。 名前で書けば、その画面はウィンドウ自身のテーマに従います。

windowBgpanelfieldBgsurfacesurfaceHoversurfacePressedbordertexttextDimaccentselectionscrimscrollbarscrollbarActive があります。

  text "saved", color: "accent"
  column(background: "panel", border_color: "border", border_width: 1.0) {  }

テーマとアニメーション

  column(theme: "dark") {  }
  segmented(options: ["read", "write"], selected: @tab,
            animate: 120.0, easing: "out") { |i| @tab = i }
  text "saved", animate: 150.0, easing: "out", enter: true

theme: は、書いた要素から下だけ配色を差し替えます。 決め打ちの文字列だけでなく変数も渡せるので、アプリの側で切り替えられます(theme: @mode)。 animate: には変化にかける時間をミリ秒で渡し、現れるときと消えるときの動きは enter:exit: で指定します。

どちらの実行も、一つの時計で動きます。 ウィンドウではフレームが、スクリプトでは advance:<ms> がその時計を進めます。 だからアニメーションは、ゲートが待つものではなく、比べられるものになります。

使い回したい見た目は、普通の Hash です。 merge して、** で渡します。

KEY = { grow: 1.0, size: 20.0, background: "panel" }.freeze
OP = KEY.merge({ background: "#fab387", color: "#1e1e2e" }).freeze

  button("7", **KEY) { press("7") }
  button("+", **OP) { press("+") }

demo/styled.rb と二つの電卓(demo/calc.rbdemo/calcgrid.rb)がその書き方です。

ウィンドウから受け取るもの

ウィンドウから受け取るものは、次の四つです。 どれも run の前に書き、一度書けばアプリが動いているあいだずっと有効です。

shortcut("cmd+s") { app.save }
menu_item("File", "Open…") { app.open }
on_key { |chord| app.typed(chord) }
on_file_drop { |path| app.load(path) }

shortcut は、キーの組み合わせにハンドラを結び付けます。 menu_item は同じハンドラを、自分で名づけたメニューの下に置きます。 on_key は、どの shortcut にも取られなかった打鍵を受け取ります。 on_file_drop は、ウィンドウに落とされたファイルの場所を受け取ります。

外の世界を読み書きするものが、もう二つあります。

  clipboard_set(@text)
  @text = clipboard_get

  job = task { open_dialog("choose a file") }
  on_done(job) { @path = task_answer }

ダイアログは人の操作を待つので、task の中に置きます(Ruby、データ、ジョブ)。 ウィンドウなしで走らせるときは、スクリプトの file:<path> がその答えになります。 だからダイアログも、ほかの操作と同じように検査できます。

demo/keys.rb が shortcut とメニューをスクリプトで動かし、demo/picker.rb がダイアログと落とされたファイルを見せます。

音は、ファイルを鳴らして、あとは放っておく形です。

  audio_play("demo/assets/sound/blip.wav")         # 録音そのままの大きさで
  audio_play("demo/assets/sound/blast.wav", 0.4)   # 0.0 から 1.0 の音量で
  audio_stop

呼び出しはすぐ返り、鳴り終わるのを待ちません。 スクリプトの下では無音になります。 ゲートが、スピーカーのあるマシンを要求してはいけないからです。 音の出ないマシンや、読めないファイルでは、アプリを止めずに何も鳴りません。 エンジンが読めるのは WAV です。 demo/sound.rb がこの機能の全部で、移植した二つのゲームもこれを使っています。

ウィンドウそのもの

run(app, title: "ledger", width: 520.0, height: 640.0, padding: 0.0)

title: はウィンドウの名前で、リリースしたときはアプリケーションバンドルの名前にもなります。 width:height: は開いたときの大きさで、0.0 なら既定のままです。 padding: は画面全体の余白で、-1.0 なら既定のまま、0.0 なら内容が端まで届きます。 キャンバスを置くときは 0.0 にします。

次に読むもの

  • Ruby、データ、ジョブ:Ruby 自身のライブラリ、データベース、タイマー、ウィンドウの外のジョブ。
  • 要素:すべての要素と、そのキーワード、型、既定値。