Current section
Files
Jump to
Current section
Files
lib/scrollable/scroll_bar.ex
defmodule FloUI.Scrollable.ScrollBar do
@moduledoc """
Scroll bars are meant to be used within the Scrollable.Container component, but you can use them to build your own scrollable containers.
The following events are sent.
`{:register_scroll_bar, direction, scroll_bar_state}`
`{:drag_changed, direction, scroll_bar_state}`
`{:scroll_bar_state_changed, direction, scroll_bar_state}`
additionally you can cast a vector2 offset to a scroll bar
`GenServer.cast(scroll_bar_pid, {update_cursor_scroll, offset})
data is an object in the form of
``` elixir
%{
direction: :vertical,
content_size: {200, 200},
width: 15,
height: 500,
scroll_position: {0, 0}
}
```
The following options are accepted
``` elixir
[
show_buttons: true,
theme: Scenic.Primitive.Style.Theme.preset(:dark),
border: 1,
radius: 3,
thickness: 15
]
```
"""
alias Scenic.Graph
alias FloUI.Scrollable.Direction
alias FloUI.Scrollable.Drag
alias FloUI.Scrollable.Wheel
alias FloUI.Scrollable.PositionCap
use SnapFramework.Component,
name: :scroll_bar,
template: "lib/scrollable/scroll_bar.eex",
controller: FloUI.Scrollable.ScrollBarController,
assigns: [],
opts: []
defcomponent(:scroll_bar, :map)
@default_drag_settings %{mouse_buttons: [:btn_left, :btn_right, :btn_middle]}
@default_id :scroll_bar
use_effect([assigns: [scroll_position: :any]],
run: [:on_scroll_position_change]
)
def setup(%{assigns: %{data: data, opts: opts}} = scene) do
scene =
assign(scene,
id: opts[:id] || @default_id,
width: data.width,
height: data.height,
direction: data.direction,
content_size: Direction.from_vector_2(data.content_size, data.direction),
frame_size: Direction.from_vector_2({data.width, data.height}, data.direction),
scroll_position: Direction.from_vector_2(data.scroll_position, data.direction),
scroll_bar_slider_background: :released,
last_scroll_position: Direction.from_vector_2(data.scroll_position, data.direction),
scroll_bar_state: %{
scrolling: :idle,
drag_state: Drag.init(opts[:scroll_drag] || @default_drag_settings),
wheel_state: %Wheel{},
scroll_buttons: nil,
pid: self()
}
)
|> init_scroll_buttons
|> init_size
|> init_position_cap
|> init_scroll_bar_background
|> init_scroll_bar_drag_control
|> init_scroll_bar_buttons
send_parent_event(
scene,
{:register_scroll_bar, scene.assigns.direction, scene.assigns.scroll_bar_state}
)
scene
end
def bounds(data, _opts) do
{0.0, 0.0, data.width, data.height}
end
def process_update(data, _opts, scene) do
{:noreply,
assign(scene,
last_scroll_position: scene.assigns.scroll_position,
scroll_position: Direction.from_vector_2(data.scroll_position, scene.assigns.direction)
)}
end
def process_input(
{:cursor_button, {button, action, _, position}},
:scroll_bar_slider_drag_control,
%{assigns: %{direction: direction, scroll_bar_state: scroll_bar_state}} = scene
) do
case action do
0 ->
unrequest_input(scene, [:cursor_pos, :cursor_button])
scrolling = :idle
scroll_position =
Direction.from_vector_2(position, direction)
|> Direction.map_horizontal(fn pos -> pos - button_width(scene) / 2 end)
|> Direction.map_vertical(fn pos -> pos - button_height(scene) / 2 end)
scroll_position = local_to_world(scene, scroll_position)
drag_state =
Drag.handle_mouse_release(
scroll_bar_state.drag_state,
button,
position
)
scroll_bar_state = %{
scroll_bar_state |
scrolling: scrolling,
drag_state: drag_state
}
scene = assign(scene,
scroll_bar_state: scroll_bar_state,
last_scroll_position: scene.assigns.scroll_position,
scroll_position: scroll_position
)
send_parent_event(scene, {:drag_changed, direction, scroll_bar_state})
{:noreply, scene}
1 ->
request_input(scene, [:cursor_pos, :cursor_button])
scrolling = :dragging
drag_state =
Drag.handle_mouse_click(
scroll_bar_state.drag_state,
button,
position,
local_scroll_position_vector2(scene)
)
scroll_bar_state = %{
scroll_bar_state |
scrolling: scrolling,
drag_state: drag_state
}
scene = assign(scene,
scroll_bar_state: scroll_bar_state
)
send_parent_event(scene, {:drag_changed, direction, scroll_bar_state})
{:noreply, scene}
end
end
def process_input(
{:cursor_pos, position},
_,
%{assigns: %{direction: direction, scroll_bar_state: scroll_bar_state}} =
scene
) do
scroll_position =
Direction.from_vector_2(position, direction)
|> Direction.map_horizontal(fn pos -> pos - button_width(scene) / 2 end)
|> Direction.map_vertical(fn pos -> pos - button_height(scene) / 2 end)
scroll_position = local_to_world(scene, scroll_position)
drag_state = Drag.handle_mouse_move(scroll_bar_state.drag_state, position)
scroll_bar_state = %{
scroll_bar_state |
drag_state: drag_state
}
scene =
assign(scene,
scroll_bar_state: scroll_bar_state,
last_scroll_position: scene.assigns.scroll_position,
scroll_position: scroll_position
)
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
{:noreply, scene}
end
def process_input(
{:cursor_pos, _},
_,
scene
) do
{:noreply, scene}
end
def process_input(
{:cursor_button, {_button, 1, _, _}},
:scroll_bar_slider_background,
scene
) do
{:noreply, scene}
end
def process_input(
{:cursor_button, {_button, 0, _, position}},
:scroll_bar_slider_background,
%{assigns: %{direction: direction}} = scene
) do
scene =
scene
|> assign(scroll_position: Direction.from_vector_2(position, direction))
send_parent_event(scene, {:update_scroll_position, direction, local_to_world(scene, position)})
{:noreply, scene}
end
def process_input(
{:cursor_button, {button, 0, _, position}},
nil,
%{assigns: %{direction: direction, scroll_bar_state: scroll_bar_state}} = scene
) do
unrequest_input(scene, [:cursor_pos, :cursor_button])
scrolling = :idle
drag_state =
Drag.handle_mouse_release(
scroll_bar_state.drag_state,
button,
position
)
scroll_bar_state = %{
scroll_bar_state |
scrolling: scrolling,
drag_state: drag_state
}
scene = assign(scene, scroll_bar_state: scroll_bar_state)
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
{:noreply, scene}
end
def process_input(
{:cursor_button, {_button, 1, _, _}},
button,
%{assigns: %{direction: direction, scroll_bar_state: scroll_bar_state}} = scene
) do
scroll_buttons = Map.update!(scroll_bar_state.scroll_buttons, button, fn _ -> :pressed end)
scrolling = :scrolling
scroll_bar_state = %{
scroll_bar_state |
scrolling: scrolling,
scroll_buttons: scroll_buttons
}
scene =
scene
|> assign(scroll_bar_state: scroll_bar_state)
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
{:noreply, scene}
end
def process_input(
{:cursor_button, {_button, 0, _, _}},
button,
%{assigns: %{direction: direction, scroll_bar_state: scroll_bar_state}} = scene
) do
scroll_buttons = Map.update!(scroll_bar_state.scroll_buttons, button, fn _ -> :released end)
scrolling = :idle
scroll_bar_state = %{
scroll_bar_state |
scrolling: scrolling,
scroll_buttons: scroll_buttons
}
scene =
scene
|> assign(scroll_bar_state: scroll_bar_state)
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
{:noreply, scene}
end
def process_cast(
{:update_cursor_scroll, {{_, offset_y}, _}},
%{assigns: %{direction: :vertical = direction, scroll_bar_state: scroll_bar_state}} = scene
) do
scene =
if offset_y == 0.0 do
scroll_bar_state = %{
scroll_bar_state |
wheel_state: Wheel.stop_scrolling(scroll_bar_state.wheel_state, {direction, 0}),
scrolling: :idle,
}
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
assign(scene, scroll_bar_state: scroll_bar_state)
else
scroll_bar_state = %{
scroll_bar_state |
wheel_state: Wheel.start_scrolling(scroll_bar_state.wheel_state, {direction, offset_y}),
scrolling: :wheel,
}
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
assign(scene, scroll_bar_state: scroll_bar_state)
end
{:noreply, scene}
end
def process_cast(
{:update_cursor_scroll, {{offset_x, _}, _}},
%{assigns: %{direction: :horizontal = direction, scroll_bar_state: scroll_bar_state}} = scene
) do
scene =
if Float.floor(offset_x) == 0 or Float.ceil(offset_x) == 0 do
scroll_bar_state = %{
scroll_bar_state |
wheel_state: Wheel.stop_scrolling(scroll_bar_state.wheel_state, {direction, offset_x}),
scrolling: :idle,
}
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
assign(scene, scroll_bar_state: scroll_bar_state)
else
scroll_bar_state = %{
scroll_bar_state |
wheel_state: Wheel.start_scrolling(scroll_bar_state.wheel_state, {direction, offset_x}),
scrolling: :wheel,
}
send_parent_event(scene, {:scroll_bar_state_changed, direction, scroll_bar_state})
assign(scene, scroll_bar_state: scroll_bar_state)
end
{:noreply, scene}
end
defp init_scroll_bar_background(
%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :vertical, opts: opts, height: height, frame_size: frame_size}} = scene
) do
scroll_bar_background_width = opts[:thickness]
scroll_bar_background_height = Direction.unwrap(height) + opts[:thickness]
scroll_bar_background_pos = {0, 0}
assign(
scene,
scroll_bar_background_width: scroll_bar_background_width,
scroll_bar_background_height: scroll_bar_background_height,
scroll_bar_background_pos: scroll_bar_background_pos
)
end
defp init_scroll_bar_background(
%{assigns: %{direction: :vertical, opts: opts, height: height, frame_size: frame_size}} = scene
) do
scroll_bar_background_width = opts[:thickness]
scroll_bar_background_height = Direction.unwrap(height)
scroll_bar_background_pos = {0, opts[:thickness]}
assign(
scene,
scroll_bar_background_width: scroll_bar_background_width,
scroll_bar_background_height: scroll_bar_background_height,
scroll_bar_background_pos: scroll_bar_background_pos
)
end
defp init_scroll_bar_background(
%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :horizontal, opts: opts, width: width, frame_size: frame_size}} = scene
) do
scroll_bar_background_height = opts[:thickness]
scroll_bar_background_width = Direction.unwrap(width) + opts[:thickness]
scroll_bar_background_pos = {0, 0}
assign(
scene,
scroll_bar_background_width: scroll_bar_background_width,
scroll_bar_background_height: scroll_bar_background_height,
scroll_bar_background_pos: scroll_bar_background_pos
)
end
defp init_scroll_bar_background(
%{assigns: %{direction: :horizontal, opts: opts, width: width, frame_size: frame_size}} = scene
) do
scroll_bar_background_height = opts[:thickness]
scroll_bar_background_width = Direction.unwrap(width)
scroll_bar_background_pos = {opts[:thickness], 0}
assign(
scene,
scroll_bar_background_width: scroll_bar_background_width,
scroll_bar_background_height: scroll_bar_background_height,
scroll_bar_background_pos: scroll_bar_background_pos
)
end
defp init_scroll_bar_drag_control(scene) do
scroll_bar_drag_control_width = button_width(scene)
scroll_bar_drag_control_height = button_height(scene)
scroll_bar_drag_control_pos = local_scroll_position_vector2(scene)
assign(
scene,
scroll_bar_drag_control_width: scroll_bar_drag_control_width,
scroll_bar_drag_control_height: scroll_bar_drag_control_height,
scroll_bar_drag_control_pos: scroll_bar_drag_control_pos
)
end
defp init_scroll_bar_buttons(%{assigns: %{direction: direction}} = scene) do
size = scroll_button_size(scene)
{button_2_x, button_2_y} =
Direction.return(size, direction)
|> Direction.add(scene.assigns.width)
|> Direction.add(scene.assigns.height)
|> Direction.to_vector_2()
scroll_button_1_width = size
scroll_button_1_height = size
scroll_button_1_pos = {0, -2}
scroll_button_2_width = size
scroll_button_2_height = size
scroll_button_2_pos = {button_2_x, button_2_y + 2}
assign(scene,
scroll_button_1_width: scroll_button_1_width,
scroll_button_1_height: scroll_button_1_height,
scroll_button_1_pos: scroll_button_1_pos,
scroll_button_2_width: scroll_button_2_width,
scroll_button_2_height: scroll_button_2_height,
scroll_button_2_pos: scroll_button_2_pos
)
end
defp init_scroll_buttons(%{assigns: %{scroll_bar_state: scroll_bar_state, opts: opts}} = scene) do
scroll_buttons =
if opts[:show_buttons] do
%{
scroll_button_1: :released,
scroll_button_2: :released
}
else
nil
end
assign(scene, scroll_bar_state: %{scroll_bar_state | scroll_buttons: scroll_buttons})
end
# defp init_size(%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, width: width, height: height, opts: opts}} = scene) do
# scene =
# assign(scene,
# width: Direction.as_horizontal(height),
# height: Direction.as_vertical(width)
# )
# displacement = scroll_bar_displacement(scene)
# assign(scene, scroll_bar_displacement: Direction.to_vector_2(displacement))
# end
defp init_size(%{assigns: %{width: width, height: height}} = scene) do
width = Direction.as_horizontal(width)
height = Direction.as_vertical(height)
displacement = scroll_bar_displacement(assign(scene, width: width, height: height))
button_size_difference = Direction.map(displacement, &(&1 * 2))
assign(scene,
width: Direction.subtract(width, button_size_difference),
height: Direction.subtract(height, button_size_difference),
scroll_bar_displacement: Direction.to_vector_2(displacement)
)
end
defp init_position_cap(%{assigns: %{direction: direction}} = scene) do
max =
Direction.return(0, direction)
|> Direction.add(scene.assigns.width)
|> Direction.add(scene.assigns.height)
|> Direction.map_horizontal(fn width ->
width - button_width(scene) + scroll_button_size(scene)
end)
|> Direction.map_vertical(fn height ->
height - button_height(scene) + scroll_button_size(scene)
end)
|> Direction.to_vector_2()
min =
scene.assigns.scroll_bar_displacement
|> Direction.to_vector_2()
assign(scene, position_cap: PositionCap.init(%{min: min, max: max}))
end
defp scroll_button_size(%{assigns: %{scroll_bar_state: %{scroll_bars: nil}}}), do: 0
defp scroll_button_size(%{assigns: %{width: width, height: height, direction: direction}}) do
Direction.return(1, direction)
|> Direction.invert()
|> Direction.multiply(width)
|> Direction.multiply(height)
|> Direction.unwrap()
end
defp button_width(%{assigns: %{direction: :horizontal, scroll_bar_state: %{scroll_buttons: nil}, frame_size: frame_size}} = scene) do
Direction.divide(scene.assigns.frame_size, scene.assigns.content_size)
|> Direction.multiply(frame_size)
|> Direction.unwrap()
end
defp button_width(%{assigns: %{direction: :horizontal}} = scene) do
Direction.divide(scene.assigns.frame_size, scene.assigns.content_size)
|> Direction.multiply(scene.assigns.width)
|> Direction.unwrap()
end
defp button_width(scene), do: scene.assigns.opts[:thickness]
defp button_height(%{assigns: %{direction: :vertical, scroll_bar_state: %{scroll_buttons: nil}, frame_size: frame_size}} = scene) do
Direction.divide(scene.assigns.frame_size, scene.assigns.content_size)
|> Direction.multiply(frame_size)
|> Direction.unwrap()
end
defp button_height(%{assigns: %{direction: :vertical}} = scene) do
Direction.divide(scene.assigns.frame_size, scene.assigns.content_size)
|> Direction.multiply(scene.assigns.height)
|> Direction.unwrap()
end
defp button_height(scene), do: scene.assigns.opts[:thickness]
defp width_factor(%{assigns: %{content_size: {:horizontal, size}, width: {_, width}}}) do
width / size
end
defp width_factor(_), do: 1
defp height_factor(%{assigns: %{content_size: {:vertical, size}, height: {_, height}}}) do
height / size
end
defp height_factor(_), do: 1
# POSITION CALCULATIONS
defp scroll_bar_displacement(%{assigns: %{direction: direction}} = scene) do
scroll_button_size(scene)
|> Direction.return(direction)
end
defp scroll_position_vector2(scene) do
Direction.to_vector_2(scene.assigns.scroll_position)
end
defp local_scroll_position_vector2(scene) do
world_to_local(scene, scroll_position_vector2(scene))
end
defp local_to_world(%{assigns: %{direction: :horizontal}} = scene, {:horizontal, x}) do
{:horizontal, local_to_world(scene, x)}
end
defp local_to_world(%{assigns: %{direction: :vertical}} = assigns, {:vertical, y}) do
{:vertical, local_to_world(assigns, y)}
end
defp local_to_world(_, {:horizontal, _}), do: {:horizontal, 0}
defp local_to_world(_, {:vertical, _}), do: {:vertical, 0}
defp local_to_world(scene, {x, y}) do
{local_to_world(scene, x), local_to_world(scene, y)}
end
defp local_to_world(_, 0), do: 0
defp local_to_world(%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :horizontal}} = scene, x) do
{x, _} = PositionCap.cap(scene.assigns.position_cap, {x, 0})
-(x) / width_factor(scene)
end
defp local_to_world(%{assigns: %{direction: :horizontal}} = scene, x) do
{x, _} = PositionCap.cap(scene.assigns.position_cap, {x, 0})
-(x - scroll_button_size(scene)) / width_factor(scene)
end
defp local_to_world(%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :vertical}} = scene, y) do
{_, y} = PositionCap.cap(scene.assigns.position_cap, {0, y})
-(y) / height_factor(scene)
end
defp local_to_world(%{assigns: %{direction: :vertical}} = scene, y) do
{_, y} = PositionCap.cap(scene.assigns.position_cap, {0, y})
-(y - scroll_button_size(scene)) / height_factor(scene)
end
defp world_to_local(%{assigns: %{direction: direction}} = scene, {x, y}) do
position =
Direction.from_vector_2({x, y}, direction)
|> Direction.map(&world_to_local(scene, &1))
|> Direction.to_vector_2()
PositionCap.cap(scene.assigns.position_cap, position)
end
defp world_to_local(%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :horizontal}} = scene, x),
do: -x * width_factor(scene)
defp world_to_local(%{assigns: %{direction: :horizontal}} = scene, x),
do: -x * width_factor(scene) + scroll_button_size(scene)
defp world_to_local(%{assigns: %{scroll_bar_state: %{scroll_buttons: nil}, direction: :vertical}} = scene, y),
do: -y * height_factor(scene)
defp world_to_local(%{assigns: %{direction: :vertical}} = scene, y),
do: -y * height_factor(scene) + scroll_button_size(scene)
end