Packages

Native desktop GUIs from Elixir, powered by iced

Retired package: Renamed - Package has been renamed to plushie: hex.pm/packages/plushie

Current section

Files

Jump to
toddy lib toddy.ex
Raw

lib/toddy.ex

defmodule Toddy do
@moduledoc """
Native desktop GUIs from Elixir, powered by iced.
## Quick start
{:ok, pid} = Toddy.start_link(MyApp)
## Dev mode (live code reloading)
{:ok, pid} = Toddy.start_link(MyApp, dev: true)
## Under a supervisor
children = [
{Toddy, app: MyApp}
]
## Options
- `:app` -- (required) the app module implementing `Toddy.App`
- `:app_opts` -- opts forwarded to `app.init/1` (default: `[]`)
- `:binary` -- path to the toddy binary (default: auto-resolved)
- `:name` -- supervisor registration name (default: `Toddy`)
- `:daemon` -- if `true`, keep running after the last window closes
(default: `false`). In daemon mode, `all_windows_closed`
is delivered to `update/2` instead of triggering shutdown.
- `:dev` -- enable live code reloading (default: `false`)
- `:dev_opts` -- options forwarded to `Toddy.DevServer` (default: `[]`)
- `:format` -- wire format, `:msgpack` (default) or `:json`
- `:log_level` -- toddy binary log level (`:off`, `:error`, `:warning`, `:info`, `:debug`).
Default: `:error`.
"""
use Supervisor
@default_binary_path :auto
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@doc """
Starts a Toddy application under a supervisor linked to the calling process.
Returns `{:ok, pid}` on success.
"""
@spec start_link(module(), keyword()) :: Supervisor.on_start()
def start_link(app_module, opts \\ []) do
opts =
opts
|> Keyword.put(:app, app_module)
|> put_instance_name()
Supervisor.start_link(__MODULE__, opts, name: sup_name(opts[:instance_name]))
end
@doc """
Stops a running Toddy supervisor.
Accepts a pid or the instance name passed as `:name` to `start_link/2`
(defaults to `Toddy`, matching the default registration).
"""
@spec stop(pid() | atom()) :: :ok
def stop(pid_or_name \\ __MODULE__)
def stop(pid) when is_pid(pid), do: Supervisor.stop(pid)
def stop(name) when is_atom(name), do: Supervisor.stop(sup_name(name))
@doc """
Child spec for embedding Toddy under an existing supervisor.
## Example
children = [
{Toddy, app: MyApp, name: :my_app_gui}
]
"""
@spec child_spec(keyword()) :: Supervisor.child_spec()
def child_spec(opts) do
opts = put_instance_name(opts)
name = opts[:instance_name]
%{
id: name,
start: {__MODULE__, :start_link, [opts[:app], opts]},
type: :supervisor
}
end
# ---------------------------------------------------------------------------
# Supervisor callbacks
# ---------------------------------------------------------------------------
@doc false
@impl true
def init(opts) do
name = opts[:instance_name]
app = Keyword.fetch!(opts, :app)
binary_path =
case Keyword.get(opts, :binary, @default_binary_path) do
:auto -> Toddy.Binary.path!()
path -> path
end
daemon? = Keyword.get(opts, :daemon, false)
dev? = Keyword.get(opts, :dev, false)
format = Keyword.get(opts, :format, :msgpack)
log_level = Keyword.get(opts, :log_level, :error)
# Bridge MUST start before Runtime. Runtime's handle_continue(:initial_render)
# fires immediately and casts Settings + Snapshot to Bridge. If Bridge hasn't
# started yet, those casts are lost and the renderer hangs.
children = [
# Bridge opens the Port and spawns the renderer process, which blocks
# on stdin waiting for a Settings message.
Supervisor.child_spec(
{Toddy.Bridge,
[
renderer_path: Path.expand(binary_path),
runtime: runtime_name(name),
name: bridge_name(name),
format: format,
log_level: log_level
]},
restart: :transient,
significant: true
),
# Runtime sends Settings then Snapshot to the already-registered Bridge.
# Bridge forwards renderer events back to Runtime.
Supervisor.child_spec(
{Toddy.Runtime,
[
app: app,
bridge: bridge_name(name),
name: runtime_name(name),
daemon: daemon?,
app_opts: Keyword.get(opts, :app_opts, [])
]},
restart: :transient,
significant: true
)
]
children =
if dev? do
dev_opts =
Keyword.get(opts, :dev_opts, [])
|> Keyword.put(:runtime, runtime_name(name))
|> Keyword.put_new(:name, dev_server_name(name))
children ++
[
Supervisor.child_spec(
{Toddy.DevServer, dev_opts},
restart: :transient
)
]
else
children
end
# :rest_for_one -- if Bridge crashes, Runtime restarts too (fresh start).
# If Runtime crashes alone, only Runtime restarts; it re-sends settings
# and snapshot to the still-running Bridge/renderer.
# :transient -- don't restart children that exit normally (clean window close).
# :auto_shutdown -- when any significant child exits normally, tear down the tree.
Supervisor.init(children,
strategy: :rest_for_one,
auto_shutdown: :any_significant
)
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp put_instance_name(opts) do
case Keyword.fetch(opts, :name) do
{:ok, name} -> Keyword.put_new(opts, :instance_name, name)
:error -> Keyword.put_new(opts, :instance_name, __MODULE__)
end
end
defp sup_name(instance_name), do: :"#{instance_name}.Supervisor"
defp runtime_name(instance_name), do: :"#{instance_name}.Runtime"
defp bridge_name(instance_name), do: :"#{instance_name}.Bridge"
defp dev_server_name(instance_name), do: :"#{instance_name}.DevServer"
end