Seeds grow in the underground

Scenic - Getting started from scratch

This post covers setting up a Scenic project in the Elixir programming language. It briefly covers the default method but largely dives into adding Scenic to an existing project, which covers the different parts that Scenic requires to run.

The official approach

Requirements

A brand new project

Scenic, the OpenGL-based and very tasty UI framework for Elixir (& friends) has a very reasonable project generator. So if you feel like trying it out. That is a great starting point.

mix archive.install hex scenic_new
mix scenic.new my_app

With this. You will have the app, my_app and some instructions for how to download and install dependencies. For completeness:

cd my_app
mix deps.get
mix scenic.run

Et voila, you have a very simple, slightly boring view and your terminal is reporting events as you move your cursor. All you could want. This is what you get from the official docs in the section Getting started. Since I can't resist poking things and had some special requirements I did things differently. So the rest of the guide is more focused on doing it from scratch and getting to know the pieces required.

Adding Scenic to an existing project

Using a basic project

I had a project and it was a Nerves project using the nerves_init_gadget and I felt like I wanted to know whatever was brought into it and understand my dependencies and config a bit. So I started from a generated Nerves project which I'm pretty familiar with. Yes, because I keep starting them and never completing them, don't you start with me, it is my spare time, I waste it however I like. But for simplicity in this guide I will start from a minimal Elixir project.

mix new my_elixir
cd my_elixir
mix test

Oh hell, that probably worked! So what do we do to get going with Scenic? A solid first step is to install it. Note: If you get lost at any point in this guide or simply cannot get a part to work you can check the code in this repository which has the completed thing.

Dependencies

We add Scenic to our dependencies. We also want to add the driver library that allows it to work on the average desktop computer which is based on glfw.

In your project, edit mix.exs, add the two dependencies:

..
defp deps do
    [
        {:scenic, "~> 0.10"},
        {:scenic_driver_glfw, "~> 0.10", targets: :host},
    ]
end
..

To install them after adding them, in the project, in your shell:

    mix deps.get

This does absolutely nothing aside from making Scenic available. But we want things happening. First we need a bit of configuration for Scenic to know what to do.

Configuration

In your project find config/config.exs and open it for editing, make it match the following:

    use Mix.Config

    # Configure the main viewport for the Scenic application
    config :my_elixir, :viewport, %{
      name: :main_viewport,
      size: {700, 600},
      default_scene: {MyElixir.Scene.Home, nil},
      drivers: [
        %{
          module: Scenic.Driver.Glfw,
          name: :glfw,
          opts: [resizeable: false, title: "my_app"]
        }
      ]
    }

Lets go through it, shall we? use Mix.Config is just standard stuff. So we add a config :my_elixir, :viewport in which :my_elixir references our app and :viewport references that this configures a Scenic Viewport. Let's not dwell on what that is. With this we add a map with a bunch of keys. Let's run through them briefly.

At this point, running will still do nothing. We have not added any running code.

The supervision tree

Let's start a supervision tree. Classic move to run anything in this day and age.

Open lib/my_elixir.ex in your editor and make it look like this:

    defmodule MyElixir do
      def start(_type, _args) do
        # load the viewport configuration from config
        main_viewport_config = Application.get_env(:my_elixir, :viewport)

        # start the application with the viewport
        children = [
          {Scenic, viewports: [main_viewport_config]}
        ]

        Supervisor.start_link(children, strategy: :one_for_one)
      end
    end

Still doesn't do a single thing. Why? Well, we aren't loading the module. So we still are not running any code. Go back to mix.exs and fix this:

    ..
    def application do
        [
          mod: {MyElixir, []},
          extra_applications: [:logger]
        ]
    end
  ..

Our app has a mod now! Or rather, our application knows which module we want to start at. I do not recommend starting it now. We have not built the default scene yet. So things will break in a rather annoying way where the supervisor will start a window over and over again because it keeps dying until you murder your app. So lets avoid that shall we?

Creating our first scene

Create a folder named scenes in lib and add a file named home.ex in that scenes folder. Open it in your editor and do something like this:

    defmodule MyElixir.Scene.Home do
      use Scenic.Scene

      alias Scenic.Graph
      # alias Scenic.ViewPort

      import Scenic.Components
      # import Scenic.Primitives

      @text_size 24

      def init(_, _opts) do
        graph =
          Graph.build(font: :roboto, font_size: @text_size)
          |> button("Click me", id: :sample_button_1, t: {32, 32})

        {:ok, graph, push: graph}
      end

      def filter_event({:click, :sample_button_1}, _from, state) do
        new_text = DateTime.to_iso8601(DateTime.utc_now())

        state =
          state
          |> Graph.modify(:sample_button_1, &button(&1, new_text))

        {:halt, state, push: state}
      end
    end

This has it all, a scene, a graph, a component and even some UI event which triggers graph modification. Oh yeah. So the general ideas are covered better by the docs. But basically, init sets up a graph that contains a component, specifically a button. The graph is "pushed" as init returns. And the scene is rendered for you in beautiful free-range GL. For more information on the ideas in Scenic I'd recommend Boyd's talk at ElixirConf 2018 where he covers most of what Scenic does and why it is cool.

Well, time to run it and see what we've made.

  iex -S mix

You should see a window with a button that updates to show the timestamp when you press it. Nothing fancy. But a start. And that is where I will end this. If you want to review the resulting code it is found here.

If you have any inquiries about this or my business, get in touch at lars@underjord.io.