# 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