#
# 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 it's 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 it's 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
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