ElixirでGUI・Scenic [新・入門編]

# １．はじめに

ElixirのGUIライブラリ「Scenic」を使って、簡単なウィンドウアプリを作ってみます。

:::note info
scenicのバージョンが0.10系から0.11系になり、過去に記述した記事がそのままではビルドできなくなりました。

- [旧Qiita記事](https://qiita.com/myasu/items/86a2038d7a8a5bf30cf6)
- [公式・Ver0.11における変更点詳細](https://hexdocs.pm/scenic/upgrading_to_v0-11.html)

本記事では、2023年時点で最新のVer.0.11系でビルドする手順を示します。
:::

## 実行環境

下記の環境で試しています。

|||
|:--|:--|
|OS|Ubuntu Linux 22.04 LTS|
|Elixir|Ver.1.15.5|

```sh:ターミナル
$ uname -a
Linux hostname 6.2.0-37-generic #38~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov  2 18:01:13 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"

$ elixir -v
Erlang/OTP 25 [erts-13.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]
Elixir 1.15.5 (compiled with Erlang/OTP 25)
```


# ２．インストール

Elixirのインストールは下記を参照願います。

https://hotch-potch.hatenadiary.jp/entry/2022/03/09/220000

Scenicのインストールは下記を参考に進めます。

https://hexdocs.pm/scenic/getting_started.html

Scenicのビルドに必要なライブラリをインストールします。

```sh:ターミナル
$ sudo apt install libglfw3 libglfw3-dev libglew-dev -y
```

scenicライブラリをインストールします。

https://hex.pm/packages/scenic

```sh:ターミナル
$ mix archive.install hex scenic_new
```

# ３．まずは"Hello World"を試す

ひな形のプロジェクトを作成して、とりあえず実行します。

```sh:ターミナル
$ cd （ご自身のワーキングディレクトリに移動）

# scenicプロジェクトを作成
$ mix scenic.new hmihello

#プロジェクトのディレクトリに移動
$ cd hmihello

# 必要なライブラリを取得
$ mix deps.get

# 実行
$ mix scenic.run
#（`iex -S mix`でもOK）
```

`[Ctrl-C]`の押下か、ウィンドウの閉じるボタンで終了できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/7eeae115-ff8b-b19a-92ac-24dface05d00.png)

# ４．テキストと図形を表示


```elixir:hmihello/lib/scenes/home.ex
（・・・省略・・・）
  # ============================================================================
  # setup

  # --------------------------------------------------------
  def init(scene, _param, _opts) do
    # get the width and height of the viewport. This is to demonstrate creating
    # a transparent full-screen rectangle to catch user input
    {width, height} = scene.viewport.size

    # show the version of scenic and the glfw driver
    scenic_ver = Application.spec(:scenic, :vsn) |> to_string()
    driver_ver = Application.spec(:scenic_driver_local, :vsn) |> to_string()

    info = "scenic: v#{scenic_ver}\nscenic_driver_local: v#{driver_ver}"

    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> add_specs_to_graph([
        # この４行をコメントアウト
        # text_spec(info, translate: {20, 40}),
        # text_spec(@note, translate: {20, 120}),
        # rect_spec({width, height})

        # ここから追記
        # テキスト
        text_spec("Hello World", t: {40, 40}),
        # 図形・丸
        #          直径　描画位置　    塗りつぶし　   輪郭
        circle_spec(10, t: {20, 30}, fill: :blue, stroke: {2, :white}),
        circle_spec(10, t: {220, 30}, fill: :yellow, stroke: {2, :red}),
        # 図形・直線
        #          対角左上　対角右下　        　   太さ　色     先端形状
        line_spec({{10, 50}, {250, 50}}, stroke: {4, :cyan}, cap: :round)
      ])

    scene = push_graph(scene, graph)

    {:ok, scene}
  end
（・・・省略・・・）
```

書き換えたら、ビルド・実行します。

```sh:ターミナル
$ mix scenic.run
```

文字と、２つの丸、直線の図形が描画できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/1f74f59d-8064-0a14-a78d-18cb96ce3a95.png)

# ５．押しボタンの入力イベントでテキストと図形を書き換え

まずは描画内容を作ります

```elixir:hmihello/lib/scenes/home.ex
defmodule Hmihello.Scene.Home do
  use Scenic.Scene
  require Logger

  alias Scenic.Graph

  import Scenic.Primitives
  import Scenic.Components          # ←ここのコメントアウトを外します

  # ↓ここは参照しないので、コメントアウトします
  # @note """
  #   This is a very simple starter application.

  #   If you want a more full-on example, please start from:

  #   mix scenic.new.example
  # """

  @text_size 24

  # ↓ウィンドウに描画する内容は、この関数にまとめます
  defp graph_build() do
    Graph.build(font: :roboto, font_size: @text_size)
    |> add_specs_to_graph(
      [
        # テキスト
        text_spec("Hello World", t: {40, 40}),
        # 図形・丸
        circle_spec(10, fill: :blue, stroke: {2, :white}, t: {20, 30}),
        circle_spec(10, fill: :yellow, stroke: {2, :red}, t: {220, 30}),
        # 図形・直線
        line_spec({{10, 50}, {250, 50}}, stroke: {4, :cyan}, cap: :round),
        # 押しボタン
        button_spec("ON", id: :btn_on, t: {40, 90}, theme: :success),
        button_spec("OFF", id: :btn_off, t: {120, 90}, theme: :danger),
        # テキスト・ON/OFF表示
        text_spec("---", t: {80, 80}, id: :event_text),
        # 図形・ランプ・ON/OFF表示
        circle_spec(10, id: :event_circle, fill: :grey, stroke: {2, :white}, t: {60, 70})
      ],
      # 全体の描画位置をオフセットする
      translate: {10, 10}
    )
  end

  # ============================================================================
  # setup

  # --------------------------------------------------------
  # ↓ここの関数はまるっと書き換えて、２行だけの内容にします
  def init(scene, _param, _opts) do
    scene = push_graph(scene, graph_build())
    {:ok, scene}
  end

  # ***あとでここに関数を書き足します***

  # ↓ここは変更なしです
  def handle_input(event, _context, scene) do
    Logger.info("Received event: #{inspect(event)}")
    {:noreply, scene}
  end
end
```

書き換えたら、ビルド・実行します。

```sh:ターミナル
$ mix scenic.run
```

先ほどの描画内容に対し、さらにON・OFFボタンと灰色のランプ、テキスト`---`が描画できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/59e070df-0656-9c66-5091-9cc25f3fa934.png)

この時点では、ボタンイベントの処理を書いていないので、ボタンをクリックしても変化はありません。

続いて、イベント処理を追加します。

先ほどのコード中に` # ***あとでここに関数を書き足します***`と書かれた箇所に、関数を２つ追記します。

```elixir:hmihello/lib/scenes/home.ex
（・・・省略・・・）

  def handle_event(event, _context, scene) do
    Logger.debug("Received handle_event: #{inspect(event)}")

    graph =
      case event do
        {:click, id} ->
          # 押しボタンのクリックイベントでマッチさせる
          graph_build()
          # 押しボタンの動作による画面更新処理に渡す
          |> on_click_button(id)

        _ ->
          # それ以外のときは書き換えなし
          graph_build()
      end

    # 描画内容を更新する
    scene =
      scene
      |> push_graph(graph)

    {:noreply, scene}
  end

  def on_click_button(graph, id) do
    case id do
      # 押しボタン・ONが押された
      :btn_on ->
        graph
        # テキストの表記を書き換え
        |> Graph.modify(:event_text, &text(&1, "ON"))
        # ランプの色を置き換え
        |> Graph.modify(:event_circle, &update_opts(&1, fill: :lawn_green))

      # 押しボタン・OFFが押された
      :btn_off ->
        graph
        |> Graph.modify(:event_text, &text(&1, "OFF"))
        |> Graph.modify(:event_circle, &update_opts(&1, fill: :grey))

      _ ->
        # それ以外のときは書き換えなし
        raise "not implimentation"
    end
  end

（・・・省略・・・）
```

書き換えたら、ビルド・実行します。

```sh:ターミナル
$ mix scenic.run
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/8802e49c-9616-1246-b143-7ca17d133554.png)

押しボタンのON・OFFを押下するごとに、テキストとランプが書き換わります。

# ６．まとめ

Ver.0.10系とVer.0.11系における変更点の気づきです。

- マウスのカーソル移動、キーボードのキー入力単体のイベントが取れなくなった
    - [Ver.0.10系での状況](https://qiita.com/myasu/items/86a2038d7a8a5bf30cf6#%EF%BC%96%E3%81%8A%E3%81%BE%E3%81%91%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E8%AA%AD%E3%81%BF%E8%A7%A3%E3%81%8D)
- `push_graph`関数で画面の更新をするようになった
- （ほか、調査中）