lib/scenic/component/input/caret.ex

#
#  Created by Boyd Multerer 2018-08-06.
#  Copyright © 2018 Kry10 Limited. All rights reserved.
#

defmodule Scenic.Component.Input.Caret do
  @moduledoc """
  Add a blinking text-input caret to a graph.

  ## Data

  `height`

  * `height` - The height of the caret. The caller (TextEdit) calculates this based
    on its :font_size (often the same thing).

  ## Options
  * `color` - any [valid color](Scenic.Primitive.Style.Paint.Color.html).

  You can change the color of the caret by setting the color option

  ```elixir
  Graph.build()
    |> caret( 20, color: :white )
  ```

  ## Usage

  The caret component is used by the TextField component and usually isn't accessed directly,
  although you are free to do so if it fits your needs. There is no short-cut helper
  function so you will need to add it to the graph manually.

  The following example adds a blue caret to a graph.

  ```elixir
  graph
    |> Caret.add_to_graph(24, id: :caret, color: :blue )
  ```
  """

  use Scenic.Component, has_children: false

  import Scenic.Primitives,
    only: [
      {:line, 3},
      {:update_opts, 2}
    ]

  alias Scenic.Graph
  alias Scenic.Primitive.Style.Theme

  @width 2
  @inset_v 4

  # caret blink speed in hertz
  @caret_hz 0.5
  @caret_ms trunc(1000 / @caret_hz / 2)

  # ============================================================================
  # setup

  # --------------------------------------------------------
  @impl Scenic.Component
  def validate(height) when is_number(height) and height >= 0 do
    {:ok, height}
  end

  def validate(data) do
    {
      :error,
      """
      #{IO.ANSI.red()}Invalid Caret specification
      Received: #{inspect(data)}
      #{IO.ANSI.yellow()}
      The data for a Caret is the height of the caret line.
      This height must be >= 0#{IO.ANSI.default_color()}
      """
    }
  end

  # --------------------------------------------------------
  @doc false
  @impl Scenic.Scene
  def init(scene, height, opts) do
    color =
      case opts[:color] do
        nil ->
          opts[:theme]
          |> Theme.normalize()
          |> Map.get(:highlight)

        c ->
          c
      end

    # build the graph, initially not showing
    # the height and the color are variable, which means it can't be
    # built at compile time
    graph =
      Graph.build()
      |> line(
        {{0, @inset_v}, {0, height - @inset_v}},
        stroke: {@width, color},
        hidden: true,
        id: :caret
      )

    scene =
      scene
      |> assign(
        graph: graph,
        hidden: true,
        timer: nil,
        focused: false
      )
      |> push_graph(graph)

    {:ok, scene}
  end

  @impl Scenic.Component
  def bounds(height, _opts) do
    {0, 0, @width, height}
  end

  # --------------------------------------------------------
  @doc false
  @impl GenServer
  def handle_cast(:start_caret, %{assigns: %{graph: graph, timer: nil}} = scene) do
    # start the timer
    {:ok, timer} = :timer.send_interval(@caret_ms, :blink)

    # show the caret
    graph = Graph.modify(graph, :caret, &update_opts(&1, hidden: false))

    scene =
      scene
      |> assign(graph: graph, hidden: false, timer: timer, focused: true)
      |> push_graph(graph)

    {:noreply, scene}
  end

  # --------------------------------------------------------
  def handle_cast(:stop_caret, %{assigns: %{graph: graph, timer: timer}} = scene) do
    # hide the caret
    graph = Graph.modify(graph, :caret, &update_opts(&1, hidden: true))

    # stop the timer
    case timer do
      nil -> :ok
      timer -> :timer.cancel(timer)
    end

    scene =
      scene
      |> assign(graph: graph, hidden: true, timer: nil, focused: false)
      |> push_graph(graph)

    {:noreply, scene}
  end

  # --------------------------------------------------------
  def handle_cast(
        :reset_caret,
        %{assigns: %{graph: graph, timer: timer, focused: true}} = scene
      ) do
    # show the caret
    graph = Graph.modify(graph, :caret, &update_opts(&1, hidden: false))

    # stop the timer
    if timer, do: :timer.cancel(timer)
    # restart the timer
    {:ok, timer} = :timer.send_interval(@caret_ms, :blink)

    scene =
      scene
      |> assign(graph: graph, hidden: false, timer: timer)
      |> push_graph(graph)

    {:noreply, scene}
  end

  # --------------------------------------------------------
  # throw away unknown messages
  # def handle_cast(_, scene), do: {:noreply, scene}

  # --------------------------------------------------------
  @doc false
  @impl GenServer
  def handle_info(:blink, %{assigns: %{graph: graph, hidden: hidden}} = scene) do
    graph = Graph.modify(graph, :caret, &update_opts(&1, hidden: !hidden))

    scene =
      scene
      |> assign(graph: graph, hidden: !hidden)
      |> push_graph(graph)

    {:noreply, scene}
  end
end