lib/scenic/primitive/component.ex

#
#  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