lib/dropdown/dropdown.ex

defmodule FloUI.Dropdown do
  @moduledoc """
  ## Usage in SnapFramework

  Dropdown component that scrolls. You can pass two separate themes as options. one for the dropdown, and one for the scroll bar.

  Options
  ``` elixir
  theme: theme,
  scroll_bar: %{
    show: true,
    show_buttons: true,
    theme: Scenic.Primitive.Style.Theme.preset(:dark),
    thickness: 15
  }
  ```

  data is a tuple in the form of

  ``` elixir
  {
    [
      {{"option 1", :option_1}, :option_1},
      {{"option 2", :option_2}, :option_2},
      {{"option 3", :option_3}, :option_3}
    ],
    :option_1
  }
  ```

  Events emitted

  `{:value_changed, id, value}`

  ``` elixir
  <%= component FloUI.Dropdown,
      {@items, @selected},
      id: :dropdown,
      theme: @theme,
      scroll_bar: %{
        show: true,
        show_buttons: true,
        theme: Scenic.Primitive.Style.Theme.preset(:dark),
        thickness: 15
      }
  %>
  ```
  """
  @default_height 50
  @default_frame_height 300
  @default_theme FloUI.Theme.preset(:base)
  @default_scroll_bar %{
    show: true,
    show_buttons: true,
    theme: Scenic.Primitive.Style.Theme.preset(:dark),
    thickness: 15
  }

  alias FloUI.Dropdown.Items

  use SnapFramework.Component,
    name: :dropdown,
    template: "lib/dropdown/dropdown.eex",
    controller: FloUI.DropdownController,
    assigns: [],
    opts: []

  defcomponent(:dropdown, :tuple)

  use_effect [assigns: [open?: :any]], [
    run: [:on_open_change]
  ]

  use_effect [assigns: [selected_label: :any]], [
    run: [:on_selected_change]
  ]

  @impl true
  def setup(%{assigns: %{data: {items, selected} = data, opts: opts}} = scene) do
    width = get_width(data, opts)
    frame_height = get_frame_height(data, opts)
    content_height = get_content_height(items)
    scroll_bar = opts[:scroll_bar] || @default_scroll_bar
    show_vertical_scroll = content_height > frame_height and scroll_bar.show

    assign(scene,
      items: items,
      selected_label: "",
      selected_key: nil,
      selected: selected,
      open?: false,
      button_width: if(show_vertical_scroll, do: width + 20, else: width),
      button_height: opts[:height] || @default_height,
      background_height: frame_height + 20,
      frame_width: if(show_vertical_scroll, do: width, else: width),
      frame_height: frame_height,
      content_height: content_height,
      scroll_bar: scroll_bar,
      show_vertical_scroll: show_vertical_scroll,
      theme: get_theme(opts)
    )
  end

  @impl true
  def bounds(data, opts) do
    {0.0, 0.0, get_width(data, opts), opts[:height] || @default_height}
  end

  @impl true
  def process_event({:value_changed, {{label, value}, key}}, _, scene) do
    {:cont, {:value_changed, scene.assigns.opts[:id], value}, assign(scene, selected_label: label, selected_key: key, open?: false)}
  end

  @impl true
  def process_input({:cursor_button, {:btn_left, 0, _, _}}, :bg, %{assigns: %{open?: open?}} = scene) do
    {:noreply, assign(scene, open?: not open?)}
  end

  def process_input({:cursor_button, {:btn_left, 1, _, _}}, :clickout, %{assigns: %{open?: open?}} = scene) do
    {:noreply, assign(scene, open?: not open?)}
  end

  def process_input(_, _, scene) do
    {:noreply, scene}
  end

  defp get_width(data, opts) do
    {_, _, w, _h} = Items.bounds(data, opts)
    w
  end

  defp get_frame_height(data, opts) do
    {_, _, _w, h} = Items.bounds(data, opts)
    frame_height = opts[:frame_height] || @default_frame_height
    if(h > frame_height, do: frame_height, else: h)
  end

  defp get_content_height(items) do
    Items.get_height(items)
  end

  defp get_theme(opts) do
    case opts[:theme] do
      nil -> @default_theme
      :dark -> @default_theme
      :light -> @default_theme
      theme -> theme
    end
    |> FloUI.Theme.normalize()
  end
end