# 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