lib/scrollbar/scroll_bars.ex

# defmodule FloUI.Scrollable.ScrollBars do
#   use Scenic.Component
#   # use FloUI.Scrollable.SceneInspector, env: [:test, :dev]

#   import FloUI.Scrollable.Components, only: [scroll_bar: 3]

#   alias Scenic.Graph

#   alias FloUI.Scrollable.ScrollBar
#   alias FloUI.Scrollable.Direction

#   @moduledoc """
#   The scroll bars component can be used to add a horizontal, and a vertical scroll bar pair to the graph. This component is used internally by the `Scenic.Scrollable` component, and for most cases it is recommended to use the `Scenic.Scrollable` component instead.

#   ## Data

#   `t:Scenic.Scrollable.ScrollBars.settings/0`

#   The scroll bars require the following data for initialization:

#   - width: number
#   - height: number
#   - content_size: `t:Scenic.Scrollable.ScrollBars.v2/0`
#   - scroll_position: number
#   - direction: :horizontal | :vertical

#   With and height define the size of the frame, and thus correspond to the width of the horizontal, and the height of the vertical scroll bars.

#   ## Styles

#   `t:Scenic.Scrollable.ScrollBars.styles/0`

#   The scroll bars can be customized by using the following styles:

#   ### scroll_bar

#   `t:Scenic.Scrollable.ScrollBar.styles/0`

#   The styles to customize both scrollbars as defined in the corresponding module `Scenic.Scrollable.Scrollbar`.
#   If different styles for the horizontal and vertical scroll bars are preffered, use the horizontal_scroll_bar and vertical_scroll_bar styles instead.

#   ### horizontal_scroll_bar

#   `t:Scenic.Scrollable.ScrollBar.styles/0`

#   The styles to customize the horizontal scroll bar.

#   ### vertical_scroll_bar

#   `t:Scenic.Scrollable.ScrollBar.styles/0`

#   The styles to customize the vertical scroll bar.

#   ### scroll_drag

#   `t:Scenic.Scrollable.Drag/0`

#   Settings to specify which mouse buttons can be used in order to drag the scroll bar sliders.

#   ### scroll_bar_thickness

#   number

#   Specify the height of the horizontal, and the width of the vertical scroll bars.

#   ## Examples

#       iex> graph = Scenic.Scrollable.Components.scroll_bars(
#       ...>   Scenic.Graph.build(),
#       ...>   %{
#       ...>     width: 200,
#       ...>     height: 200,
#       ...>     content_size: {1000, 1000},
#       ...>     scroll_position: {0, 0}
#       ...>   },
#       ...>   [
#       ...>     scroll_bar: [
#       ...>       scroll_buttons: true,
#       ...>       scroll_bar_theme: Scenic.Primitive.Style.Theme.preset(:light),
#       ...>       scroll_bar_radius: 2,
#       ...>       scroll_bar_border: 2,
#       ...>       scroll_drag: %{
#       ...>         mouse_buttons: [:left, :right, :middle]
#       ...>       }
#       ...>     ],
#       ...>     scroll_drag: %{
#       ...>       mouse_buttons: [:left, :right, :middle]
#       ...>     },
#       ...>     id: :scroll_bars_component_1
#       ...>   ]
#       ...> )
#       ...> graph.primitives[1].id
#       :scroll_bars_component_1
#   """

#   @typedoc """
#   Data structure representing a vector 2, in the form of an {x, y} tuple.
#   """
#   @type v2 :: Scenic.Scrollable.v2()

#   @type content_size :: {number, number}

#   @typedoc """
#   The required settings to initialize a scroll bars component.
#   For more information see the top of this module.
#   """
#   @type settings :: %{
#           width: number,
#           height: number,
#           content_size: v2,
#           scroll_position: v2
#         }

#   @typedoc """
#   The optional styles to customize the scroll bars.
#   For more information see the top of this module.
#   """
#   @type style ::
#           {:scroll_bar, Scenic.Scrollable.ScrollBar.styles()}
#           | {:horizontal_scroll_bar, Scenic.Scrollable.ScrollBar.styles()}
#           | {:vertical_scroll_bar, Scenic.Scrollable.ScrollBar.styles()}
#           | {:scroll_drag, Scenic.Scrollable.Drag.settings()}
#           | {:scroll_bar_thickness, number}

#   @typedoc """
#   A collection of optional styles to customize the scroll bars.
#   For more information see `t:Scenic.Scrollable.ScrollBars.style/0` and the top of this module.
#   """
#   @type styles :: [style]

#   @typedoc """
#   An atom describing the state the scroll bars are in.
#   - idle: none of the scroll bars are currently being clicked or dragged.
#   - dragging: one of the scroll bars is being dragged.
#   - scrolling: one of the scroll bars is being scrolled using a scroll button.
#   """
#   @type scroll_state ::
#           :idle
#           | :dragging
#           | :scrolling

#   @typedoc """
#   The state with which the scrollable components GenServer is running.
#   """
#   @type t :: %__MODULE__{
#           id: atom,
#           graph: Graph.t(),
#           scroll_position: v2,
#           content_size: content_size,
#           scroll_state: scroll_state,
#           pid: pid,
#           horizontal_scroll_bar_pid: {:some, pid} | :none,
#           vertical_scroll_bar_pid: {:some, pid} | :none
#         }

#   defstruct id: :scroll_bars,
#             graph: Graph.build(),
#             scroll_position: {0, 0},
#             content_size: {0, 0},
#             scroll_state: :idle,
#             pid: nil,
#             horizontal_scroll_bar_pid: :none,
#             vertical_scroll_bar_pid: :none

#   @default_id :scroll_bars
#   @default_thickness 10

#   # PUBLIC API

#   @doc """
#   Find the direction the content should be scrolling in, depending on the scroll bar buttons pressed states.
#   """
#   @spec direction(t) :: v2
#   def direction(state) do
#     {x, _} =
#       state.horizontal_scroll_bar_pid
#       |> OptionEx.map(&ScrollBar.direction/1)
#       |> OptionEx.or_else({0, 0})

#     {_, y} =
#       state.vertical_scroll_bar_pid
#       |> OptionEx.map(&ScrollBar.direction/1)
#       |> OptionEx.or_else({0, 0})

#     {x, y}
#   end

#   @doc """
#   Find out if one of the scroll bars is currently being dragged.
#   """
#   @spec dragging?(t) :: boolean
#   def dragging?(%{scroll_state: :dragging}), do: true

#   def dragging?(_), do: false

#   @doc """
#   Find out if one of the scroll bars wheel is currently being scrolled.
#   """
#   @spec wheel_scrolling?(t) :: boolean
#   def wheel_scrolling?(%{wheel_state: :scrolling}), do: true

#   def wheel_scrolling?(_), do: false

#   @doc """
#   Find the latest position the scrollable content should be updated with.
#   The position corresponds to the contents translation, rather than the scroll bars drag control translation.
#   """
#   @spec new_position(t) :: {:some, v2} | :none
#   def new_position(%{scroll_position: position}), do: {:some, position}

#   # CALLBACKS

#   @impl Scenic.Scene
#   def init(scene, settings, opts) do
#     id = opts[:id] || @default_id
#     styles = Enum.into(opts || %{}, [])
#     shared_styles = Keyword.take(styles, [:scroll_bar, :scroll_drag])

#     horizontal_bar_styles =
#       (styles[:horizontal_scroll_bar] || styles[:scroll_bar])
#       |> OptionEx.return()
#       |> OptionEx.map(&Keyword.merge(&1, shared_styles))
#       |> OptionEx.map(&Keyword.put(&1, :id, :horizontal_scroll_bar))
#       |> OptionEx.map(&Keyword.put(&1, :translate, {0, settings.height}))

#     vertical_bar_styles =
#       (styles[:vertical_scroll_bar] || styles[:scroll_bar])
#       |> OptionEx.return()
#       |> OptionEx.map(&Keyword.merge(&1, shared_styles))
#       |> OptionEx.map(&Keyword.put(&1, :id, :vertical_scroll_bar))
#       |> OptionEx.map(&Keyword.put(&1, :translate, {settings.width, 0}))

#     {content_width, content_height} = settings.content_size
#     {x, y} = settings.scroll_position

#     graph = Graph.build()

#     graph =
#       horizontal_bar_styles
#       |> OptionEx.map(fn styles ->
#         graph
#         |> FloUI.Scrollable.ScrollBar.add_to_graph(
#           %{
#             width: settings.width,
#             height: styles[:scroll_bar_thickness] || @default_thickness,
#             content_size: content_width,
#             scroll_position: x,
#             direction: :horizontal
#           },
#           styles
#         )
#       end)
#       |> OptionEx.or_else(graph)

#     graph =
#       vertical_bar_styles
#       |> OptionEx.map(fn styles ->
#         graph
#         |> FloUI.Scrollable.ScrollBar.add_to_graph(
#           %{
#             width: styles[:scroll_bar_thickness] || @default_thickness,
#             height: settings.height,
#             content_size: content_height,
#             scroll_position: y,
#             direction: :vertical
#           },
#           styles
#         )
#       end)
#       |> OptionEx.or_else(graph)

#     state = %__MODULE__{
#       id: id,
#       graph: graph,
#       scroll_position: {x, y},
#       content_size: {content_width, content_height},
#       pid: self()
#     }

#     scene =
#       scene
#       |> assign(state: state)
#       |> push_graph(state.graph)

#     send_parent_event(scene, {:scroll_bars_initialized, state.id, state})

#     {:ok, scene}
#   end

#   @impl Scenic.Component
#   def validate(
#         %{
#           content_size: {content_x, content_y},
#           scroll_position: {x, y}
#         } = settings
#       )
#       when is_number(content_x) and is_number(content_y) and is_number(x) and is_number(y) do
#     {:ok, settings}
#   end

#   def validate(_), do: :invalid_input

#   @impl Scenic.Scene
#   def handle_event(
#         {:scroll_bar_initialized, :horizontal_scroll_bar, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = %{state | horizontal_scroll_bar_pid: OptionEx.return(scroll_bar_state.pid)}
#     {:noreply, assign(scene, state: state)}
#   end

#   def handle_event(
#         {:scroll_bar_initialized, :vertical_scroll_bar, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = %{state | vertical_scroll_bar_pid: OptionEx.return(scroll_bar_state.pid)}
#     {:noreply, assign(scene, state: state)}
#   end

#   def handle_event(
#         {:scroll_bar_button_pressed, _, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = update_scroll_state(state, scroll_bar_state)
#     {:cont, {:scroll_bars_button_pressed, state.id, state}, assign(scene, state: state)}
#   end

#   def handle_event(
#         {:scroll_bar_button_released, _, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = update_scroll_state(state, scroll_bar_state)

#     {:cont, {:scroll_bars_button_released, state.id, state, scroll_bar_state.wheel_state},
#      assign(scene, state: state)}
#   end

#   def handle_event(
#         {:cursor_scroll_started, _, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = update_scroll_state(state, scroll_bar_state)

#     {:cont, {:cursor_scroll_started, state.id, state, scroll_bar_state.wheel_state},
#      assign(scene, state: state)}
#   end

#   def handle_event(
#         {:cursor_scroll_stopped, _, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = update_scroll_state(state, scroll_bar_state)

#     {:cont, {:cursor_scroll_stopped, state.id, state, scroll_bar_state.wheel_state},
#      assign(scene, state: state)}
#   end

#   def handle_event(
#         {:scroll_bar_position_change, _, _scroll_bar_state},
#         _from,
#         %{assigns: %{state: %{scroll_state: :scrolling} = state}} = scene
#       ) do
#     {:noreply, scene}
#   end

#   def handle_event(
#         {:scroll_bar_position_change, _, %{direction: direction} = scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     {x, y} = state.scroll_position

#     state =
#       ScrollBar.new_position(scroll_bar_state)
#       |> Direction.from_vector_2(direction)
#       |> Direction.map_horizontal(&{&1, y})
#       |> Direction.map_vertical(&{x, &1})
#       |> Direction.unwrap()
#       |> (&Map.put(state, :scroll_position, &1)).()
#       |> update_scroll_state(scroll_bar_state)

#     {:cont, {:scroll_bars_position_change, state.id, state}, assign(scene, state: state)}
#   end

#   def handle_event(
#         {:scroll_bar_scroll_end, _id, scroll_bar_state},
#         _from,
#         %{assigns: %{state: state}} = scene
#       ) do
#     state = update_scroll_state(state, scroll_bar_state)

#     {:cont, {:scroll_bars_scroll_end, state.id, state}, assign(scene, state: state)}
#   end

#   def handle_event(_event, _from, scene) do
#     {:noreply, scene}
#   end

#   # no callback on the `Scenic.Scene` and no GenServer @behaviour, so impl will not work
#   @spec handle_call(request :: term(), GenServer.from(), state :: term()) ::
#           {:reply, reply :: term(), new_state :: term()}
#   def handle_call({:update_scroll_position, {x, y}}, _, %{assigns: %{state: state}} = scene) do
#     state = %{state | scroll_position: {x, y}}
#     # TODO error handling
#     state.horizontal_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_scroll_position, x}) end)

#     state.vertical_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_scroll_position, y}) end)

#     {:reply, :ok, assign(scene, state: state)}
#   end

#   def handle_call({:update_scroll_pos, {x, y}}, _, %{assigns: %{state: state}} = scene) do
#     state = %{state | scroll_position: {x, y}}
#     # TODO error handling
#     state.horizontal_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_scroll_position, x}) end)

#     state.vertical_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_scroll_position, y}) end)

#     {:reply, :ok, assign(scene, state: state)}
#   end

#   def handle_call({:update_content_size, {width, height}}, _, %{assigns: %{state: state}} = scene) do
#     state = %{state | content_size: {width, height}}

#     state.horizontal_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_content_size, width}) end)

#     state.vertical_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.call(pid, {:update_content_size, height}) end)

#     {:reply, :ok, assign(scene, state: state)}
#   end

#   def handle_call(msg, _, scene) do
#     {:reply, {:error, {:unexpected_message, msg}}, scene}
#   end

#   def handle_cast({:update_cursor_scroll, scroll_pos}, %{assigns: %{state: state}} = scene) do
#     # TODO error handling
#     state.horizontal_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.cast(pid, {:update_cursor_scroll, scroll_pos}) end)

#     state.vertical_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.cast(pid, {:update_cursor_scroll, scroll_pos}) end)

#     {:noreply, scene}
#   end

#   def handle_cast(:unrequest_cursor_scroll, %{assigns: %{state: state}} = scene) do
#     # TODO error handling
#     state.horizontal_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.cast(pid, :unrequest_cursor_scroll) end)

#     state.vertical_scroll_bar_pid
#     |> OptionEx.map(fn pid -> GenServer.cast(pid, :unrequest_cursor_scroll) end)

#     {:noreply, scene}
#   end

#   # UTILITY

#   @spec update_scroll_state(t, ScrollBar.t()) :: t
#   defp update_scroll_state(state, scroll_bar_state) do
#     %{state | scroll_state: scroll_bar_state.scroll_state}
#   end
# end