Scenic - Getting started from scratch
2019-05-20Underjord is a tiny, wholesome team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.
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
- Elixir 18, OTP 21 (I recommend using asdf to install and use the right version)
- Scenic 0.10 (this is covered in the guide)
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.
-
name
Should make things prettier in observer. Supposedly optional. -
size
This is the resolution/size of the scenic window. This you can change for fun and profit. -
default_scene
It has to start somewhere, a scene in Scenic is a specific view in your application. This is what it shows by default. -
drivers
This comes with some sub-configuration which we needn't go into now. But the module key tells us what driver we are configuring. This would be different if you were targeting the official touch screen for Raspberry Pi for example. This driver was provided by thescenic_driver_glfw
package we installed earlier.
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.
Underjord is a 4 people team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.
Note: Or try the videos on the YouTube channel.