#
# Created by Boyd Multerer on 2021-02-03.
# Copyright © 2021 Kry10 Limited. All rights reserved.
#
defmodule Scenic.Primitive.Component do
@moduledoc """
Add a child component to a graph.
When a scene pushes a graph containing a Component to its ViewPort,
a new scene, containing the component, is created and added as a child
to the scene that created it.
Any events the new component creates are sent up the parent. The parent
can use functions in the `Scenic.Scene` module to manage its children,
send them messages and such.
The standard components, such as button, slider, etc. have wrapper functions
making them very easy to add to a graph. However, if you have a custom
component you can add it to any graph manually using the `add_to_graph/3`
function.
You typically want to give components an `:id`. This will be used to identify
events coming from that components back to your scene.
```elixir
import Components # contains the button helper
graph
|> button("Press Me", id: :press_me)
|> MyComponent.add_to_graph({"Some data", 123}, id: :my_component)
```
"""
use Scenic.Primitive
alias Scenic.Script
alias Scenic.Primitive
alias Scenic.Primitive.Style
@type t :: {mod :: module, param :: any, name :: atom | String.t()}
@type styles_t :: [:hidden | :scissor]
@styles [:hidden, :scissor]
# longer names use more memory, but have a lower chance of collision.
# 16 should still have a very very very low chance of collision
# (16 * 8) = 128 bits of randomness
@name_length 16
@main_id Scenic.ViewPort.main_id()
# ============================================================================
# data verification and serialization
@impl Primitive
@spec validate(
{mod :: module, param :: any}
| {mod :: module, param :: any, name :: String.t()}
) ::
{:ok, {mod :: module, param :: any, name :: String.t()}}
| {:error, String.t()}
def validate({mod, param}) do
name =
@name_length
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
validate({mod, param, name})
end
# special case the root
def validate({@main_id, nil, @main_id}), do: {:ok, {@main_id, nil, @main_id}}
def validate({mod, param, name})
when is_atom(mod) and mod != nil and
(is_pid(name) or is_atom(name) or is_bitstring(name)) and name != nil do
case mod.validate(param) do
{:ok, data} -> {:ok, {mod, data, name}}
err -> err
end
end
def validate(data) do
{
:error,
"""
#{IO.ANSI.red()}Invalid Component specification
Received: #{inspect(data)}
#{IO.ANSI.yellow()}
The specification for a component is { module, param } or { module, param, name }
If you do not supply a name, a random string will be chosen for you.#{IO.ANSI.default_color()}
"""
}
end
# --------------------------------------------------------
# filter and gather styles
@doc """
Returns a list of styles recognized by this primitive.
"""
@impl Primitive
@spec valid_styles() :: styles :: styles_t()
def valid_styles(), do: @styles
# --------------------------------------------------------
# compiling a component is a special case and is handled in Scenic.Graph.Compiler
@doc false
@impl Primitive
@spec compile(primitive :: Primitive.t(), styles :: Style.t()) :: Script.t()
def compile(%Primitive{module: __MODULE__}, _styles) do
raise "compiling a Component is a special case and is handled in Scenic.Graph.Compiler"
end
# --------------------------------------------------------
@doc false
def default_pin({@main_id, _data, @main_id}, _styles), do: {0, 0}
def default_pin({module, data, _name}, styles) when is_atom(module) do
case Kernel.function_exported?(module, :default_pin, 2) do
true -> module.default_pin(data, styles)
false -> {0, 0}
end
end
end