Current section
Files
Jump to
Current section
Files
lib/scrollable/container.ex
defmodule FloUI.Scrollable.ScrollableContainer do
@moduledoc false
alias Scenic.Graph
import Scenic.Primitives, only: [group: 3]
alias Scenic.Math.Vector2
alias FloUI.Scrollable.Hotkeys
alias FloUI.Scrollable.Drag
alias FloUI.Scrollable.Wheel
alias FloUI.Scrollable.ScrollBars
alias FloUI.Scrollable.Acceleration
alias FloUI.Scrollable.PositionCap
use SnapFramework.Component,
name: :scrollable_container,
template: "lib/scrollable/container.eex",
controller: :none,
assigns: [],
opts: []
defcomponent :scrollable_container, :map
@default_position {0, 0}
@default_fps 30
def setup(%{assigns: %{data: data, opts: opts}} = scene) do
Logger.debug(inspect data)
{content_width, content_height} = data.content
{frame_width, frame_height} = data.frame
{frame_x, frame_y} = opts[:translate] || @default_position
scroll_position = opts[:scroll_position] || @default_position
assign(scene,
id: opts[:id] || :scrollable,
content_builder: opts[:content_builder],
frame: %{x: frame_x, y: frame_y, width: frame_width, height: frame_height},
content: %{x: 0, y: 0, width: content_width, height: content_height},
scroll_position: Vector2.add(scroll_position, {0, 0}),
fps: opts[:scroll_fps] || @default_fps,
acceleration: Acceleration.init(opts[:scroll_acceleration]),
hotkeys: Hotkeys.init(opts[:scroll_hotkeys]),
drag_state: Drag.init(opts[:scroll_drag]),
wheel_state: %Wheel{},
scroll_bars_state: nil,
scroll_bars: opts[:scroll_bars]
)
|> init_position_caps
end
def process_event({:scroll_bar_initialized, _scroll_bar_state}, _, scene) do
scene = init_content(scene)
{:noreply, scene}
end
def process_event({:scroll_bar_button_pressed, direction, scroll_buttons}, _, scene) do
scene = update(scene)
{:noreply, scene}
end
defp init_position_caps(
%{assigns:
%{
frame: %{width: frame_width, height: frame_height},
content: %{x: x, y: y, width: content_width, height: content_height}
}
} = scene
) do
min = {x + frame_width - content_width, y + frame_height - content_height}
max = {x, y}
position_cap = PositionCap.init(%{min: min, max: max})
assign(scene,
position_caps: position_cap,
scroll_position: PositionCap.cap(position_cap, scene.assigns.scroll_position)
)
end
defp init_content(%{assigns: %{graph: graph, content_builder: content_builder, scroll_position: scroll_position, frame: frame, content: content}} = scene) do
graph =
Scenic.Primitives.group(
graph,
&group(&1, content_builder, [id: :content, translate: Vector2.add(scroll_position, {content.x, content.y})]),
id: :frame,
scissor: {frame.width, frame.height},
translate: {frame.x, frame.y}
)
assign(scene, graph: graph)
|> push_graph(graph)
end
defp update(scene) do
scene
|> update_scroll_state
|> apply_force
|> translate
# |> update_scroll_bars
|> tick
# push_graph(scene, state.graph)
# state
end
defp update_scroll_state(scene) do
scrolling =
verify_idle_state(scene)
|> OptionEx.or_try(fn -> verify_dragging_state(scene) end)
|> OptionEx.or_try(fn -> verify_scrolling_state(scene) end)
|> OptionEx.or_try(fn -> verify_wheel_state(scene) end)
|> OptionEx.or_try(fn -> verify_cooling_down_state(scene) end)
assign(scene, scrolling: scrolling)
# |> OptionEx.map(&%{scene.assigns | scrolling: &1})
# |> OptionEx.or_else(scene)
end
defp apply_force(%{assigns: %{scrolling: :idle}} = scene), do: scene
defp apply_force(%{assigns: %{scrolling: :dragging}} = scene) do
scroll_position =
scene.assigns.scroll_bars
|> OptionEx.bind(&OptionEx.from_bool(ScrollBars.dragging?(&1), &1))
|> OptionEx.bind(&ScrollBars.new_position/1)
|> OptionEx.map(fn new_position ->
Vector2.add(new_position, {scene.assigns.content.x, scene.assigns.content.y})
end)
|> OptionEx.or_try(fn ->
OptionEx.from_bool(Drag.dragging?(scene.assigns.drag_state), scene.assigns.drag_state)
|> OptionEx.bind(&Drag.new_position/1)
end)
assign(scene, scroll_position: PositionCap.cap(scene.assigns.position_caps, scroll_position))
# |> OptionEx.map(&%{state | scroll_position: PositionCap.cap(state.position_caps, &1)})
# |> OptionEx.or_else(state)
end
defp apply_force(%{assigns: %{scrolling: :wheel, wheel_state: %{offset: {:vertical, offset_y}}}} = scene) do
{x, y} = scene.assigns.scroll_position
scroll_position = {x, y + offset_y * 10}
assign(scene, scroll_position: PositionCap.cap(scene.assigns.position_caps, scroll_position))
# %{state | scroll_position: PositionCap.cap(state.position_caps, scroll_position)}
end
defp apply_force(%{assigns: %{scrolling: :wheel, wheel_state: %{offset: {:horizontal, offset_x}}}} = scene) do
{x, y} = scene.assigns.scroll_position
scroll_position = {x + offset_x * 10, y}
assign(scene, scroll_position: PositionCap.cap(scene.assigns.position_caps, scroll_position))
# %{state | scroll_position: PositionCap.cap(state.position_caps, scroll_position)}
end
# defp apply_force(%{assigns: assigns} = scene) do
# force =
# Hotkeys.direction(assigns.hotkeys)
# |> Vector2.add(get_scroll_bars_direction(scene))
# Acceleration.apply_force(assigns.acceleration, force)
# |> Acceleration.apply_counter_pressure()
# |> (&%{assigns | acceleration: &1}).()
# |> (fn assigns ->
# scroll_pos = Acceleration.translate(assigns.acceleration, scene.assigns.scroll_position)
# assign(scene, scroll_position: PositionCap.cap(assigns.position_caps, scroll_pos))
# end).()
# end
defp translate(%{assigns: %{graph: graph, content: %{x: x, y: y}}} = scene) do
graph =
graph
|> Graph.modify(:content, &Scenic.Primitive.put_transform(&1, :translate, Vector2.add(scene.assigns.scroll_position, {x, y})))
assign(scene, graph: graph)
|> push_graph(graph)
# Map.update!(state, :graph, fn graph ->
# graph
# |> Graph.modify(:content, fn primitive ->
# Map.update(primitive, :transforms, %{}, fn styles ->
# Map.put(styles, :translate, Vector2.add(state.scroll_position, {x, y}))
# end)
# end)
# end)
end
defp verify_idle_state(%{assigns: assigns} = scene) do
result =
Hotkeys.direction(assigns.hotkeys) == {0, 0} and not
Drag.dragging?(assigns.drag_state) and
assigns.wheel_state.wheel_state != :scrolling and
# get_scroll_bars_direction(assigns) == {0, 0} and not
# scroll_bars_dragging?(scene) and
Acceleration.is_stationary?(assigns.acceleration)
OptionEx.from_bool(result, :idle)
end
defp verify_dragging_state(%{assigns: assigns} = scene) do
result = Drag.dragging?(scene.assigns.drag_state)
# or scroll_bars_dragging?(state)
OptionEx.from_bool(result, :dragging)
end
defp verify_scrolling_state(%{assigns: assigns} = scene) do
result =
Hotkeys.direction(assigns.hotkeys) != {0, 0}
# or (get_scroll_bars_direction(scene) != {0, 0} and not (assigns.scrolling == :dragging))
OptionEx.from_bool(result, :scrolling)
end
defp verify_wheel_state(%{assigns: assigns} = scene) do
{_, offset} = assigns.wheel_state.offset
result =
not Hotkeys.is_any_key_pressed?(assigns.hotkeys) and
not Drag.dragging?(assigns.drag_state) and
offset > 0 or offset < 0
# and get_scroll_bars_direction(scene) == {0, 0} and
# not scroll_bars_dragging?(scene)
OptionEx.from_bool(result, :wheel)
end
defp verify_cooling_down_state(%{assigns: assigns} = scene) do
{_, offset} = scene.wheel_state.offset
result =
not Hotkeys.is_any_key_pressed?(assigns.hotkeys) and
not Drag.dragging?(assigns.drag_state) and
offset == 0
# and get_scroll_bars_direction(scene) == {0, 0} and
# not scroll_bars_dragging?(scene) and
not Acceleration.is_stationary?(assigns.acceleration)
OptionEx.from_bool(result, :cooling_down)
end
defp start_cooling_down(%{assigns: assigns} = scene, cursor_pos) do
speed =
Drag.last_position(assigns.drag_state)
|> OptionEx.or_else(cursor_pos)
|> (&Vector2.sub(cursor_pos, &1)).()
|> (&Drag.amplify_speed(assigns.drag_state, &1)).()
assign(scene, acceleration: Acceleration.set_speed(assigns.acceleration, speed))
# Map.update!(state, :acceleration, &Acceleration.set_speed(&1, speed))
end
defp capture_focus(%{assigns: %{focused: false}} = scene) do
capture_input(scene, :key)
assign(scene, focused: true)
end
defp capture_focus(state, _), do: state
defp release_focus(%{assigns: %{focused: true}} = scene) do
release_input(scene)
assign(scene, focused: false)
end
defp release_focus(scene), do: scene
defp tick(%{assigns: %{scrolling: :idle}} = scene), do: assign(scene, animating: false)
defp tick(%{assigns: %{scrolling: :dragging}} = scene), do: assign(scene, animating: false)
defp tick(%{assigns: %{scrolling: :wheel}} = scene), do: assign(scene, animating: false)
defp tick(%{assigns: %{animating: true}} = scene), do: scene
defp tick(scene) do
Process.send_after(self(), :tick, tick_time(scene))
assign(scene, animating: true)
end
defp tick_time(%{fps: fps}) do
trunc(1000 / fps)
end
end